diff --git a/invenio_cli/cli/assets.py b/invenio_cli/cli/assets.py index 28ebc889..ea57a85e 100644 --- a/invenio_cli/cli/assets.py +++ b/invenio_cli/cli/assets.py @@ -8,7 +8,6 @@ """Invenio module to ease the creation and management of applications.""" import click -from click_default_group import DefaultGroup from ..commands import AssetsCommands from .utils import pass_cli_config, run_steps diff --git a/invenio_cli/cli/cli.py b/invenio_cli/cli/cli.py index 5f4532b8..39896448 100644 --- a/invenio_cli/cli/cli.py +++ b/invenio_cli/cli/cli.py @@ -45,8 +45,15 @@ def invenio_cli(ctx): help='Check development requirements.') def check_requirements(development): """Checks the system fulfills the pre-requirements.""" + try: + # the 'check-requirements' command should probably be available + # from outside the project directory as well + cli_config = CLIConfig() + except FileNotFoundError: + cli_config = None + click.secho("Checking pre-requirements...", fg="green") - steps = RequirementsCommands.check(development) + steps = RequirementsCommands(cli_config).check(development) on_fail = "Pre requisites not met." on_success = "All requisites are fulfilled." @@ -54,17 +61,19 @@ def check_requirements(development): @invenio_cli.command() -def shell(): +@pass_cli_config +def shell(cli_config): """Shell command.""" - Commands.shell() + Commands(cli_config).shell() @invenio_cli.command() +@pass_cli_config @click.option('--debug/--no-debug', '-d/', default=False, is_flag=True, help='Enable Flask development mode (default: disabled).') -def pyshell(debug): +def pyshell(cli_config, debug): """Python shell command.""" - Commands.pyshell(debug=debug) + Commands(cli_config).pyshell(debug=debug) @invenio_cli.command() @@ -151,9 +160,10 @@ def destroy(cli_config): @click.option('--script', required=True, help='The path of custom migration script.' ) -def upgrade(script): +@pass_cli_config +def upgrade(cli_config, script): """Upgrades the current instance to a newer version.""" - steps = UpgradeCommands.upgrade(script) + steps = UpgradeCommands(cli_config).upgrade(script) on_fail = "Upgrade failed." on_success = "Upgrade sucessfull." diff --git a/invenio_cli/cli/packages.py b/invenio_cli/cli/packages.py index 0135d1e4..9d50ee61 100644 --- a/invenio_cli/cli/packages.py +++ b/invenio_cli/cli/packages.py @@ -32,7 +32,7 @@ def lock(cli_config, pre, dev): f"Include dev-packages: {dev}.", fg="green" ) - steps = PackagesCommands.lock(pre, dev) + steps = PackagesCommands(cli_config).lock(pre, dev) on_fail = "Failed to lock dependencies." on_success = "Dependencies locked successfully." @@ -49,7 +49,7 @@ def install(cli_config, packages, skip_build): if len(packages) < 1: raise click.UsageError("You must specify at least one package.") - steps = PackagesCommands.install_packages(packages) + steps = PackagesCommands(cli_config).install_packages(packages) on_fail = f"Failed to install packages {packages}." on_success = f"Packages {packages} installed successfully." @@ -67,8 +67,9 @@ def install(cli_config, packages, skip_build): @pass_cli_config def outdated(cli_config): """Show outdated Python dependencies.""" - steps = PackagesCommands.outdated_packages() + steps = PackagesCommands(cli_config).outdated_packages() + # TODO poetry still exits with 0, even if there's outdated packages on_fail = "Some of the packages need to be updated." on_success = "All packages are up to date." @@ -80,15 +81,16 @@ def outdated(cli_config): @pass_cli_config def update(cli_config, version=None): """Update all or some Python python packages.""" + pkg_commands = PackagesCommands(cli_config) if version: db = cli_config.get_db_type() es = f"elasticsearch{cli_config.get_es_version()}" package = f"invenio-app-rdm[{db},{es}]~=" - steps = PackagesCommands.update_package_new_version(package, version) + steps = pkg_commands.update_package_new_version(package, version) on_fail = f"Failed to update version {version}" on_success = f"Version {version} installed successfully." else: - steps = PackagesCommands.update_packages() + steps = pkg_commands.update_packages() on_fail = "Failed to update packages." on_success = "Packages installed successfully." diff --git a/invenio_cli/cli/services.py b/invenio_cli/cli/services.py index f6740e04..11bacc95 100644 --- a/invenio_cli/cli/services.py +++ b/invenio_cli/cli/services.py @@ -10,7 +10,7 @@ import click -from ..commands import Commands, ServicesCommands +from ..commands import ServicesCommands from .utils import pass_cli_config, run_steps diff --git a/invenio_cli/commands/assets.py b/invenio_cli/commands/assets.py index af302598..f67f6c9c 100644 --- a/invenio_cli/commands/assets.py +++ b/invenio_cli/commands/assets.py @@ -9,16 +9,16 @@ import subprocess -from functools import partial from pathlib import Path import click from pynpm import NPMPackage from ..helpers import env +from ..helpers.packaging import get_packaging_backend from ..helpers.process import ProcessResponse, run_interactive from .local import LocalCommands -from .steps import CommandStep, FunctionStep +from .steps import FunctionStep class AssetsCommands(LocalCommands): @@ -79,7 +79,7 @@ def _npm_install_command(path): ) else: return ProcessResponse( - error=f"Unable to install dependent packages. " + error="Unable to install dependent packages. " "Got error code {status_code}", status_code=status_code ) @@ -125,8 +125,9 @@ def _assets_link(assets_pkg, module_pkg): def watch_assets(self): """High-level command to watch assets for changes.""" # Commands - prefix = ['pipenv', 'run'] - watch_cmd = prefix + ['invenio', 'webpack', 'run', 'start'] + watch_cmd = get_packaging_backend(self.cli_config).run_command( + "invenio", "webpack", "run", "start" + ) with env(FLASK_ENV='development'): # Collect into statics/ and assets/ folder diff --git a/invenio_cli/commands/commands.py b/invenio_cli/commands/commands.py index 5371d0e7..23d0c475 100644 --- a/invenio_cli/commands/commands.py +++ b/invenio_cli/commands/commands.py @@ -8,6 +8,7 @@ """Invenio module to ease the creation and management of applications.""" from ..helpers.env import env +from ..helpers.packaging import get_packaging_backend from ..helpers.process import run_interactive from .steps import CommandStep @@ -22,17 +23,17 @@ def __init__(self, cli_config): """ self.cli_config = cli_config - @classmethod - def shell(cls): + def shell(self): """Start a shell in the virtual environment.""" - command = ['pipenv', 'shell', ] + command = get_packaging_backend(self.cli_config).shell_command() return run_interactive(command, env={'PIPENV_VERBOSITY': "-1"}) - @classmethod - def pyshell(cls, debug=False): + def pyshell(self, debug=False): """Start a Python shell.""" with env(FLASK_ENV='development' if debug else 'production'): - command = ['pipenv', 'run', 'invenio', 'shell'] + command = get_packaging_backend(self.cli_config).run_command( + "invenio", "shell" + ) return run_interactive(command, env={'PIPENV_VERBOSITY': "-1"}) def destroy(self): @@ -41,9 +42,10 @@ def destroy(self): NOTE: This function has no knowledge of the existence of services. Refer to services.py to destroy services' containers. """ + command = get_packaging_backend(self.cli_config).remove_venv_command() steps = [ CommandStep( - cmd=['pipenv', '--rm'], + cmd=command, env={'PIPENV_VERBOSITY': "-1"}, message="Destroying virtual environment" ) diff --git a/invenio_cli/commands/containers.py b/invenio_cli/commands/containers.py index 63f9298a..0714f9ce 100644 --- a/invenio_cli/commands/containers.py +++ b/invenio_cli/commands/containers.py @@ -10,7 +10,6 @@ from ..helpers.docker_helper import DockerHelper from .packages import PackagesCommands from .services import ServicesCommands -from .services_health import HEALTHCHECKS from .steps import FunctionStep @@ -23,6 +22,7 @@ def __init__(self, cli_config, docker_helper=None): DockerHelper(cli_config.get_project_shortname(), local=False) super(ContainersCommands, self).__init__(cli_config, docker_helper) + self.pkg_commands = PackagesCommands(cli_config) def build(self, pull=True, cache=True): """Return the steps to build images. @@ -32,7 +32,7 @@ def build(self, pull=True, cache=True): """ steps = [ FunctionStep( - func=PackagesCommands.is_locked, + func=self.pkg_commands.is_locked, message="Checking if dependencies are locked." ), FunctionStep( @@ -81,7 +81,8 @@ def _cleanup(self, project_shortname="/opt/var/instance/"): }, message="Purging queues..." ), - FunctionStep(func=self.cli_config.update_services_setup, + FunctionStep( + func=self.cli_config.update_services_setup, args={"is_setup": False}, message="Updating service setup status (False)..." ) @@ -227,7 +228,7 @@ def start(self, lock=False, build=False, setup=False, demo_data=True, if lock: # FIXME: Should this params be accepted? sensible defaults? - steps.extend(PackagesCommands.lock(pre=True, dev=True)) + steps.extend(self.pkg_commands.lock(pre=True, dev=True)) if build: steps.extend(self.build()) diff --git a/invenio_cli/commands/install.py b/invenio_cli/commands/install.py index 804c7359..405dc038 100644 --- a/invenio_cli/commands/install.py +++ b/invenio_cli/commands/install.py @@ -7,11 +7,12 @@ """Invenio module to ease the creation and management of applications.""" -from ..helpers import env, filesystem +from ..helpers import filesystem +from ..helpers.packaging import get_packaging_backend from ..helpers.process import run_cmd from .local import LocalCommands from .packages import PackagesCommands -from .steps import CommandStep, FunctionStep +from .steps import FunctionStep class InstallCommands(LocalCommands): @@ -20,16 +21,17 @@ class InstallCommands(LocalCommands): def __init__(self, cli_config): """Constructor.""" super(InstallCommands, self).__init__(cli_config) + self.pkg_commands = PackagesCommands(cli_config) def install_py_dependencies(self, pre, dev=False): """Install Python dependencies.""" # If not locked, lock. Then install. steps = [] - if PackagesCommands.is_locked().status_code > 0: - steps.extend(PackagesCommands.lock(pre, dev)) + if self.pkg_commands.is_locked().status_code > 0: + steps.extend(self.pkg_commands.lock(pre, dev)) - steps.extend(PackagesCommands.install_locked_dependencies(pre, dev)) + steps.extend(self.pkg_commands.install_locked_dependencies(pre, dev)) return steps @@ -38,10 +40,11 @@ def update_instance_path(self): # FIXME: Transform into steps. # Currently not possible because the second step (update instance # path) requires the ouptut of the previous step - result = run_cmd( - ['pipenv', 'run', 'invenio', 'shell', '--no-term-title', - '-c', '"print(app.instance_path, end=\'\')"'] + command = get_packaging_backend(self.cli_config).run_command( + 'invenio', 'shell', '--no-term-title', '-c', + '"print(app.instance_path, end=\'\')"', ) + result = run_cmd(command) if result.status_code == 0: self.cli_config.update_instance_path(result.output.strip()) result.output = "Instance path updated successfully." @@ -67,21 +70,21 @@ def install(self, pre, dev=False, flask_env='production'): FunctionStep( func=self.symlink_project_file_or_folder, args={"target": 'invenio.cfg'}, - message=f"Symlinking 'invenio.cfg'..." + message="Symlinking 'invenio.cfg'..." ) ) steps.append( FunctionStep( func=self.symlink_project_file_or_folder, args={"target": 'templates'}, - message=f"Symlinking 'templates'..." + message="Symlinking 'templates'..." ) ) steps.append( FunctionStep( func=self.symlink_project_file_or_folder, args={"target": 'app_data'}, - message=f"Symlinking 'app_data'..." + message="Symlinking 'app_data'..." ) ) steps.append( diff --git a/invenio_cli/commands/local.py b/invenio_cli/commands/local.py index 493f8ec3..52fb853c 100644 --- a/invenio_cli/commands/local.py +++ b/invenio_cli/commands/local.py @@ -11,11 +11,12 @@ from distutils.dir_util import copy_tree from os import environ from pathlib import Path -from subprocess import Popen as popen +from subprocess import Popen import click from ..helpers import env, filesystem +from ..helpers.packaging import get_packaging_backend from ..helpers.process import ProcessResponse, run_interactive from .commands import Commands from .services import ServicesCommands @@ -68,7 +69,7 @@ def update_statics_and_assets(self, force, flask_env='production'): Needed here (parent) because is used by Assets and Install commands. """ # Commands - prefix = ['pipenv', 'run'] + prefix = get_packaging_backend(self.cli_config).run_command() collect_cmd = prefix + ['invenio', 'collect', '--verbose'] clean_create_cmd = prefix + ['invenio', 'webpack', 'clean', 'create'] create_cmd = prefix + ['invenio', 'webpack', 'create'] @@ -119,22 +120,26 @@ def signal_handler(sig, frame): ServicesCommands(self.cli_config).ensure_containers_running() click.secho("Starting celery worker...", fg="green") - worker = popen([ - 'pipenv', 'run', 'celery', '--app', - 'invenio_app.celery', 'worker', '--beat', '--events', - '--loglevel', 'INFO' - ]) + command = get_packaging_backend(self.cli_config).run_command( + 'celery', '--app', 'invenio_app.celery', 'worker', + '--beat', '--events', '--loglevel', 'INFO', + ) + worker = Popen(command) click.secho("Starting up local (development) server...", fg='green') run_env = environ.copy() run_env['FLASK_ENV'] = 'development' if debug else 'production' run_env['INVENIO_SITE_UI_URL'] = f"https://{host}:{port}" run_env['INVENIO_SITE_API_URL'] = f"https://{host}:{port}/api" - server = popen([ - 'pipenv', 'run', 'invenio', 'run', '--cert', - 'docker/nginx/test.crt', '--key', 'docker/nginx/test.key', - '--host', host, '--port', port - ], env=run_env) + + command = get_packaging_backend(self.cli_config).run_command( + 'invenio', 'run', + '--cert', 'docker/nginx/test.crt', + '--key', 'docker/nginx/test.key', + '--host', host, + '--port', port + ) + server = Popen(command, env=run_env) click.secho( f'Instance running!\nVisit https://{host}:{port}', diff --git a/invenio_cli/commands/packages.py b/invenio_cli/commands/packages.py index 57865961..3f7e2019 100644 --- a/invenio_cli/commands/packages.py +++ b/invenio_cli/commands/packages.py @@ -9,21 +9,20 @@ from os import listdir +from ..helpers.packaging import get_packaging_backend from ..helpers.process import ProcessResponse +from .commands import Commands from .steps import CommandStep -class PackagesCommands(object): +class PackagesCommands(Commands): """Local installation commands.""" - @staticmethod - def install_packages(packages): - """Steps to install Python packages. - - It is a class method since it does not require any configuration. - """ - prefix = ['pipenv', 'run'] - cmd = prefix + ['pip', 'install'] + def install_packages(self, packages): + """Steps to install Python packages.""" + cmd = get_packaging_backend(self.cli_config).run_command( + "pip", "install" + ) for package in packages: cmd.extend(['-e', package]) @@ -37,13 +36,10 @@ def install_packages(packages): return steps - @staticmethod - def outdated_packages(): - """Steps to show outdated packages. - - It is a class method since it does not require any configuration. - """ - cmd = ['pipenv', 'update', '--outdated'] + def outdated_packages(self): + """Steps to show outdated packages.""" + pkg_backend = get_packaging_backend(self.cli_config) + cmd = pkg_backend.outdated_packages_command() steps = [ CommandStep( @@ -55,13 +51,9 @@ def outdated_packages(): return steps - @staticmethod - def update_packages(): - """Steps to update all Python packages. - - It is a class method since it does not require any configuration. - """ - cmd = ['pipenv', 'update'] + def update_packages(self): + """Steps to update all Python packages.""" + cmd = get_packaging_backend(self.cli_config).update_command() steps = [ CommandStep( @@ -73,18 +65,14 @@ def update_packages(): return steps - @staticmethod - def update_package_new_version(package, version): - """Update invenio-app-rdm version. - - It is a class method since it does not require any configuration. - """ - prefix = ['pipenv'] - app = prefix + ['install', package + version] + def update_package_new_version(self, package, version): + """Update invenio-app-rdm version.""" + pkg = package + version + cmd = get_packaging_backend(self.cli_config).add_package_command(pkg) steps = [ CommandStep( - cmd=app, + cmd=cmd, env={'PIPENV_VERBOSITY': "-1"}, message=f"Updating {package} to version {version}..." ) @@ -92,15 +80,12 @@ def update_package_new_version(package, version): return steps - @staticmethod - def install_locked_dependencies(pre, dev): + def install_locked_dependencies(self, pre, dev): """Install dependencies from Pipfile.lock using sync.""" # NOTE: sync has no interactive process feedback - cmd = ['pipenv', 'sync'] - if pre: - cmd += ['--pre'] - if dev: - cmd += ['--dev'] + # TODO: check if the `pre` flag makes sense here + pkg_backend = get_packaging_backend(self.cli_config) + cmd = pkg_backend.install_dependencies_command(dev) steps = [ CommandStep( @@ -113,14 +98,11 @@ def install_locked_dependencies(pre, dev): return steps - @staticmethod - def lock(pre, dev): + def lock(self, pre, dev): """Steps to lock Python dependencies.""" - cmd = ['pipenv', 'lock'] - if pre: - cmd += ['--pre'] - if dev: - cmd += ['--dev'] + # TODO check the `pre` flag with poetry + pkg_backend = get_packaging_backend(self.cli_config) + cmd = pkg_backend.lock_dependencies_command(dev, pre) steps = [ CommandStep( @@ -132,10 +114,12 @@ def lock(pre, dev): return steps - @staticmethod - def is_locked(): + def is_locked(self): """Checks if the dependencies have been locked.""" - locked = 'Pipfile.lock' in listdir('.') + pkg_backend = get_packaging_backend(self.cli_config) + lock_file_name = pkg_backend.lock_file_name + locked = lock_file_name in listdir('.') + if not locked: return ProcessResponse( error="Dependencies were not locked. " + diff --git a/invenio_cli/commands/requirements.py b/invenio_cli/commands/requirements.py index 22f33187..c550e737 100644 --- a/invenio_cli/commands/requirements.py +++ b/invenio_cli/commands/requirements.py @@ -10,13 +10,14 @@ import re import sys -from os import listdir -from ..helpers.process import ProcessResponse, run_cmd, run_interactive +from ..commands import Commands +from ..helpers.packaging import get_packaging_backend +from ..helpers.process import ProcessResponse, run_cmd from .steps import FunctionStep -class RequirementsCommands(object): +class RequirementsCommands(Commands): """Pre-requirements check.""" @classmethod @@ -144,6 +145,25 @@ def check_imagemagick_version(cls, major, minor=-1, patch=-1, status_code=1, ) + @classmethod + def check_poetry_installed(cls): + """Check the poetry version.""" + # Output in the form of 'Poetry (version 1.2.0b1)', or + # 'Poetry version 1.1.13' + result = run_cmd(["poetry", "--version"]) + pattern = r"[Pp]oetry \(?version ([A-Za-z0-9\.]+)\)?" + match = re.search(pattern, result.output) + if not match: + return ProcessResponse( + f"Poetry not found. Got {result.error}.", + status_code=1, + ) + + return ProcessResponse( + output=f"Poetry OK. Got version {match.group(1)}.", + status_code=0, + ) + @classmethod def check_pipenv_installed(cls): """Check the pipenv version.""" @@ -177,31 +197,40 @@ def check_dev(cls): return steps - @classmethod - def check(cls, development=False): + def check(self, development=False): """Steps to check the pre-requisites.""" + # check if the configured packaging backend is available + pkg_backend = get_packaging_backend(self.cli_config) + if pkg_backend.bin_name == "poetry": + packaging_tool_check = FunctionStep( + func=self.check_poetry_installed, + message="Checking if Poetry is installed..." + ) + else: + packaging_tool_check = FunctionStep( + func=self.check_pipenv_installed, + message="Checking if Pipenv is installed..." + ) + steps = [ FunctionStep( - func=cls.check_python_version, + func=self.check_python_version, args={"major": 3, "minor": 6}, message="Checking Python version..." ), + packaging_tool_check, FunctionStep( - func=cls.check_pipenv_installed, - message="Checking Pipenv is installed..." - ), - FunctionStep( - func=cls.check_docker_version, + func=self.check_docker_version, args={"major": 0, "minor": 0}, message="Checking Docker version..." ), FunctionStep( - func=cls.check_docker_compose_version, + func=self.check_docker_compose_version, args={"major": 1, "minor": 17}, message="Checking Docker Compose version..." ), FunctionStep( - func=cls.check_imagemagick_version, + func=self.check_imagemagick_version, args={"major": 0, "minor": 0}, message="Checking ImageMagick version...", skippable=True, @@ -209,6 +238,6 @@ def check(cls, development=False): ] if development: - steps.extend(cls.check_dev()) + steps.extend(self.check_dev()) return steps diff --git a/invenio_cli/commands/services.py b/invenio_cli/commands/services.py index 0f83feb8..f9ab02c4 100644 --- a/invenio_cli/commands/services.py +++ b/invenio_cli/commands/services.py @@ -9,6 +9,7 @@ """Invenio module to ease the creation and management of applications.""" from ..helpers.docker_helper import DockerHelper +from ..helpers.packaging import get_packaging_backend from ..helpers.process import ProcessResponse from .commands import Commands from .services_health import HEALTHCHECKS, ServicesHealthCommands @@ -49,42 +50,43 @@ def services_expected_status(self, expected): ) return ProcessResponse( - output=f"Services setup status consistent.", + output="Services setup status consistent.", status_code=0 ) def _cleanup(self): """Services cleanup steps.""" + prefix = get_packaging_backend(self.cli_config).run_command("invenio") steps = [ CommandStep(cmd=[ - 'pipenv', 'run', 'invenio', 'shell', '--no-term-title', '-c', + *prefix, 'shell', '--no-term-title', '-c', "import redis; redis.StrictRedis.from_url(app.config['CACHE_REDIS_URL']).flushall(); print('Cache cleared')"], # noqa env={'PIPENV_VERBOSITY': "-1"}, message="Flushing Redis...", skippable=True ), CommandStep( - cmd=['pipenv', 'run', 'invenio', 'db', 'destroy', - '--yes-i-know'], + cmd=[*prefix, 'db', 'destroy', '--yes-i-know'], env={'PIPENV_VERBOSITY': "-1"}, message="Destroying database...", skippable=True ), CommandStep( - cmd=['pipenv', 'run', 'invenio', 'index', 'destroy', + cmd=[*prefix, 'index', 'destroy', '--force', '--yes-i-know'], env={'PIPENV_VERBOSITY': "-1"}, message="Destroying indices...", skippable=True ), CommandStep( - cmd=['pipenv', 'run', 'invenio', 'index', 'queue', + cmd=[*prefix, 'index', 'queue', 'init', 'purge'], env={'PIPENV_VERBOSITY': "-1"}, message="Purging queues...", skippable=True ), - FunctionStep(func=self.cli_config.update_services_setup, + FunctionStep( + func=self.cli_config.update_services_setup, args={"is_setup": False}, message="Updating service setup status (False)..." ) @@ -103,6 +105,7 @@ def _default_location_path(self): def _setup(self): """Services initialization steps.""" + prefix = get_packaging_backend(self.cli_config).run_command("invenio") steps = [ FunctionStep( func=self.services_expected_status, @@ -110,30 +113,30 @@ def _setup(self): message="Checking services are not setup..." ), CommandStep( - cmd=['pipenv', 'run', 'invenio', 'db', 'init', 'create'], + cmd=[*prefix, 'db', 'init', 'create'], env={'PIPENV_VERBOSITY': "-1"}, message="Creating database..." ), CommandStep( - cmd=['pipenv', 'run', 'invenio', 'files', 'location', + cmd=[*prefix, 'files', 'location', 'create', '--default', 'default-location', self._default_location_path()], env={'PIPENV_VERBOSITY': "-1"}, message="Creating files location..." ), CommandStep( - cmd=['pipenv', 'run', 'invenio', 'roles', 'create', 'admin'], + cmd=[*prefix, 'roles', 'create', 'admin'], env={'PIPENV_VERBOSITY': "-1"}, message="Creating admin role..." ), CommandStep( - cmd=['pipenv', 'run', 'invenio', 'access', 'allow', + cmd=[*prefix, 'access', 'allow', 'superuser-access', 'role', 'admin'], env={'PIPENV_VERBOSITY': "-1"}, message="Allowing superuser access to admin role..." ), CommandStep( - cmd=['pipenv', 'run', 'invenio', 'index', 'init'], + cmd=[*prefix, 'index', 'init'], env={'PIPENV_VERBOSITY': "-1"}, message="Creating indices..." ), @@ -148,9 +151,11 @@ def _setup(self): def demo(self): """Steps to add demo records into the instance.""" + pkg_backend = get_packaging_backend(self.cli_config) + command = pkg_backend.run_command("invenio", "rdm-records", "demo") steps = [ CommandStep( - cmd=['pipenv', 'run', 'invenio', 'rdm-records', 'demo'], + cmd=command, env={'PIPENV_VERBOSITY': "-1"}, message="Creating demo records..." ) @@ -160,7 +165,8 @@ def demo(self): def fixtures(self): """Steps to set up the required fixtures for the instance.""" - command = ['pipenv', 'run', 'invenio', 'rdm-records', 'fixtures'] + pkg_backend = get_packaging_backend(self.cli_config) + command = pkg_backend.run_command("invenio", "rdm-records", "fixtures") steps = [ CommandStep( cmd=command, diff --git a/invenio_cli/commands/services_health.py b/invenio_cli/commands/services_health.py index 827edca4..71b6d5d0 100644 --- a/invenio_cli/commands/services_health.py +++ b/invenio_cli/commands/services_health.py @@ -27,33 +27,24 @@ class ServicesHealthCommands(object): """Services status commands.""" @classmethod - def es_healthcheck(cls, *args, **kwargs): + def es_healthcheck(cls, *args, verbose, **kwargs): """Elasticsearch healthcheck.""" - verbose = kwargs['verbose'] - return run_cmd([ "curl", "-f", "localhost:9200/_cluster/health?wait_for_status=yellow" ]) @classmethod - def postgresql_healthcheck(cls, *args, **kwargs): + def postgresql_healthcheck(cls, *args, verbose, filepath, **kwargs): """Postgresql healthcheck.""" - filepath = kwargs['filepath'] - verbose = kwargs['verbose'] - return run_cmd([ "docker-compose", "--file", filepath, "exec", "-T", "db", "bash", "-c", "pg_isready", ]) @classmethod - def mysql_healthcheck(cls, *args, **kwargs): + def mysql_healthcheck(cls, *args, verbose, filepath, password, **kwargs): """Mysql healthcheck.""" - filepath = kwargs['filepath'] - verbose = kwargs['verbose'] - password = kwargs['project_shortname'] - return run_cmd([ "docker-compose", "--file", filepath, "exec", "-T", "db", "bash", "-c", @@ -61,11 +52,8 @@ def mysql_healthcheck(cls, *args, **kwargs): ]) @classmethod - def redis_healthcheck(cls, *args, **kwargs): + def redis_healthcheck(cls, *args, verbose, filepath, **kwargs): """Redis healthcheck.""" - filepath = kwargs['filepath'] - verbose = kwargs['verbose'] - return run_cmd([ "docker-compose", "--file", filepath, "exec", "-T", "cache", "bash", "-c", diff --git a/invenio_cli/commands/upgrade.py b/invenio_cli/commands/upgrade.py index beab0143..e94e040f 100644 --- a/invenio_cli/commands/upgrade.py +++ b/invenio_cli/commands/upgrade.py @@ -7,14 +7,15 @@ """Invenio module to ease the creation and management of applications.""" +from ..helpers.packaging import get_packaging_backend +from .commands import Commands from .steps import CommandStep -class UpgradeCommands(object): +class UpgradeCommands(Commands): """Local installation commands.""" - @staticmethod - def upgrade(script_path): + def upgrade(self, script_path): """Steps to perform an upgrade of the invenio instance. First, and alembic upgrade is launched to allow alembic to migrate the @@ -23,7 +24,7 @@ def upgrade(script_path): Last, the Elasticsearch indexes are destroyed, initialized and rebuilt. It is a class method since it does not require any configuration. """ - prefix = ['pipenv', 'run', 'invenio'] + prefix = get_packaging_backend(self.cli_command).run_command("invenio") alembic_cmd = prefix + ['alembic', 'upgrade'] destroy_index_cmd = prefix + ['index', 'destroy', '--yes-i-know'] init_index_cmd = prefix + ['index', 'init'] diff --git a/invenio_cli/helpers/__init__.py b/invenio_cli/helpers/__init__.py index b0eb9abd..a8d9485f 100644 --- a/invenio_cli/helpers/__init__.py +++ b/invenio_cli/helpers/__init__.py @@ -13,3 +13,10 @@ from .docker_helper import DockerHelper from .env import env from .filesystem import get_created_files + +__all__ = ( + "CookiecutterWrapper", + "DockerHelper", + "env", + "get_created_files", +) diff --git a/invenio_cli/helpers/cli_config.py b/invenio_cli/helpers/cli_config.py index c2f32e7a..18c1e6e7 100644 --- a/invenio_cli/helpers/cli_config.py +++ b/invenio_cli/helpers/cli_config.py @@ -57,7 +57,7 @@ def __init__(self, project_dir='./'): try: with open(self.private_config_path) as cfg_file: self.private_config.read_file(cfg_file) - except FileNotFoundError as e: + except FileNotFoundError: CLIConfig._write_private_config(Path(project_dir)) with open(self.private_config_path) as cfg_file: self.private_config.read_file(cfg_file) @@ -125,6 +125,12 @@ def get_file_storage(self): """Returns the file storage (local, s3, etc.).""" return self.config[CLIConfig.COOKIECUTTER_SECTION]['file_storage'] + def get_packaging_backend(self): + """Return the configured packaging backend (poetry/pipenv).""" + return self.config[CLIConfig.COOKIECUTTER_SECTION].get( + 'packaging_backend', 'pipenv' + ) + @classmethod def _write_private_config(cls, project_dir): """Write per-instance config file.""" diff --git a/invenio_cli/helpers/packaging.py b/invenio_cli/helpers/packaging.py new file mode 100644 index 00000000..6b790f66 --- /dev/null +++ b/invenio_cli/helpers/packaging.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2022 TU Wien. +# +# Invenio is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Wrappers for packaging tools.""" + +from abc import ABC, abstractmethod + + +class PackagingBackend(ABC): + """Base wrapper for packaging tools.""" + + bin_name = None + + def run_command(self, *args): + """Get the command for running a command inside the venv.""" + return [self.bin_name, "run", *args] + + def shell_command(self): + """Get the command for activating the venv.""" + return [self.bin_name, "shell"] + + def update_command(self): + """Get the command for updating the locked packages.""" + return [self.bin_name, "update"] + + @abstractmethod + def outdated_packages_command(self): + """Get the command to look up outdated packages.""" + raise NotImplementedError() + + @abstractmethod + def add_package_command(self, *args, pre_release=False): + """Get the command for installing a new package.""" + raise NotImplementedError() + + @abstractmethod + def lock_dependencies_command(self, dev_packages, pre_releases): + """Get the command for locking the packages.""" + raise NotImplementedError() + + @abstractmethod + def install_dependencies_command(self, dev_packages): + """Get the command for installing the locked packages.""" + raise NotImplementedError() + + @abstractmethod + def remove_venv_command(self): + """Get the command for removing the created venv.""" + raise NotImplementedError() + + @property + @abstractmethod + def lock_file_name(self): + """Get the name of the lock file.""" + raise NotImplementedError() + + +class PipenvBackend(PackagingBackend): + """Pipenv packaging backend.""" + + bin_name = "pipenv" + + def outdated_packages_command(self): + """Get the command to look up outdated packages.""" + return [self.bin_name, "update", "--outdated"] + + def add_package_command(self, *args, pre_release=False): + """Get the command for installing a new package.""" + cmd = [self.bin_name, "install"] + if pre_release: + cmd += ["--pre"] + + return [*cmd, *args] + + def lock_dependencies_command(self, dev_packages, pre_releases): + """Get the command for locking the packages.""" + cmd = [self.bin_name, "lock"] + if dev_packages: + cmd += ["--dev"] + if pre_releases: + cmd += ["--pre"] + + return cmd + + def install_dependencies_command(self, dev_packages): + """Get the command for installing the locked packages.""" + cmd = [self.bin_name, "install"] + if dev_packages: + cmd += ["--dev"] + + return cmd + + def remove_venv_command(self): + """Get the command for removing the created venv.""" + return [self.bin_name, "--rm"] + + @property + def lock_file_name(self): + """Get the name of the lock file.""" + return "Pipfile.lock" + + +class PoetryBackend(PackagingBackend): + """Poetry packaging backend.""" + + bin_name = "poetry" + + def outdated_packages_command(self): + """Get the command to look up outdated packages.""" + return [self.bin_name, "show", "--outdated"] + + def add_package_command(self, *args, pre_release=False): + """Get the command for installing a new package.""" + cmd = [self.bin_name, "add"] + if pre_release: + cmd += ["--allow-prereleases"] + + return [*cmd, *args] + + def lock_dependencies_command(self, dev_packages, pre_releases): + """Get the command for locking the packages.""" + # it seems that `poetry lock` doesn't accept `--dev` or `--pre` flags + # TODO verify! + return [self.bin_name, "lock"] + + def install_dependencies_command(self, dev_packages): + """Get the command for installing the locked packages.""" + cmd = [self.bin_name, "install"] + if not dev_packages: + # NOTE: this is being deprecated in poetry 1.2.x + cmd += ["--no-dev"] + + return cmd + + def remove_venv_command(self): + """Get the command for removing the created venv.""" + return [self.bin_name, "env", "remove", "--all", "--quiet"] + + @property + def lock_file_name(self): + """Get the name of the lock file.""" + return "poetry.lock" + + +def get_packaging_backend(config): + """Get the packaging backend specified in the given configuration. + + If no configuration is specified, or if it has an invalid value + configured, the ``PipenvBackend`` will be returned as a fallback. + """ + if config is not None: + backend_name = config.get_packaging_backend() + if backend_name.lower() == "poetry": + return PoetryBackend() + elif backend_name.lower() == "pipenv": + return PipenvBackend() + + # the default value is pipenv, as that's been used so far + return PipenvBackend() diff --git a/invenio_cli/helpers/process.py b/invenio_cli/helpers/process.py index 173de144..ca4ba2b4 100644 --- a/invenio_cli/helpers/process.py +++ b/invenio_cli/helpers/process.py @@ -10,9 +10,7 @@ """Invenio CLI Process helper module.""" from os import environ -from subprocess import PIPE, CalledProcessError -from subprocess import Popen as popen -from subprocess import run +from subprocess import PIPE, CalledProcessError, Popen, run class ProcessResponse(): @@ -31,7 +29,7 @@ def __init__(self, output=None, error=None, status_code=0, warning=False): def run_cmd(command): """Runs a given command and returns a ProcessResponse.""" - p = popen(command, stdout=PIPE, stderr=PIPE) + p = Popen(command, stdout=PIPE, stderr=PIPE) output, error = p.communicate() output = output.decode("utf-8") error = error.decode("utf-8") @@ -52,9 +50,10 @@ def run_interactive(command, env=None, skippable=False): full_env[var] = val try: - response = run(command, check=True, env=full_env) + run(command, check=True, env=full_env) return ProcessResponse( - output=None, error=None, status_code=0) + output=None, error=None, status_code=0 + ) except CalledProcessError as e: if skippable: return ProcessResponse(