diff --git a/docs/conf.py b/docs/conf.py index 2f4d095277..639f89b25e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,6 +47,7 @@ "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinxcontrib.spelling", + "sphinx_tabs.tabs", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/how-to-guides/hpc.rst b/docs/how-to-guides/hpc.rst index cd1994cf89..e312d1d298 100644 --- a/docs/how-to-guides/hpc.rst +++ b/docs/how-to-guides/hpc.rst @@ -12,7 +12,7 @@ By default all workflows are executed by the ``cwltool`` provider, that exports the workflow to CWL and then uses `cwltool `_ to execute the given CWL. -The workflow backend can be changed by using the ``-p/--provider `` +The workflow backend can be changed by using the ``-p/--provider `` command line option. A backend's default configuration can be overridden by providing the ``-c/--config `` command line parameter. The following ``renku`` commands support the above mentioned workflow provider @@ -27,7 +27,7 @@ simply would run the following command: .. code-block:: console - $ renku execute -p toil my_plan + $ renku workflow execute -p toil my_plan Using ``toil`` as a workflow provider has the advantage that it supports running the workflows on various `high-performance computing `_ @@ -57,7 +57,7 @@ Taking the example above, the following command line will execute ``my_plan`` on .. code-block:: console - $ TOIL_SLURM_ARGS="-A my_account --export=ALL" renku execute -p toil -c provider.yaml my_plan + $ TOIL_SLURM_ARGS="-A my_account --export=ALL" renku workflow execute -p toil -c provider.yaml my_plan where diff --git a/docs/how-to-guides/index.rst b/docs/how-to-guides/index.rst index 52b75b5beb..2b6e207c1b 100644 --- a/docs/how-to-guides/index.rst +++ b/docs/how-to-guides/index.rst @@ -13,3 +13,4 @@ aimed at active users of Renku CLI and target specific use-cases or common issue hpc provider + shell-integration diff --git a/docs/how-to-guides/shell-integration.rst b/docs/how-to-guides/shell-integration.rst new file mode 100644 index 0000000000..f4707a1700 --- /dev/null +++ b/docs/how-to-guides/shell-integration.rst @@ -0,0 +1,46 @@ +.. _shell-integration: + +Shell integration of Renku CLI +============================== + +Renku CLI supports shell auto-completion for Renku commands and their arguments like datasets and workflows. + +To activate tab completion for your supported shell run the following command after installing Renku CLI: + +.. tabs:: + + .. tab:: bash + + .. code-block:: console + + $ eval "$(_RENKU_COMPLETE=bash_source renku)" + + .. tab:: fish + + .. code-block:: console + + $ eval (env _RENKU_COMPLETE=fish_source renku) + + .. tab:: zsh + + .. code-block:: console + + $ eval "$(_RENKU_COMPLETE=zsh_source renku)" + +After this not only sub-commands of ``renku`` will be auto-completed using tab, but for example +in case of ``renku workflow execute`` the available ``Plans`` are going to be listed. + +.. code-block:: console + + $ renku workflow execute run + run1 run10 run11 run12 run13 run14 run2 run3 run4 run7 run8 + +.. note:: + + Tab completion of available ``Plans`` only works if the user is executing the command + within a Renku project. + + +For more information on how to set up shell auto-completion, see documentation for the Click library, +which used under the hood by Renku CLI: +`shell completion `_ diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 85bd6fd66b..c0ba8ac238 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -249,4 +249,5 @@ worktrees yaml yml Zenodo +zsh 連句 diff --git a/poetry.lock b/poetry.lock index 661c493525..7a1a3e6f24 100644 --- a/poetry.lock +++ b/poetry.lock @@ -330,20 +330,6 @@ python-versions = ">=3.6" colorama = {version = "*", markers = "platform_system == \"Windows\""} importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -[[package]] -name = "click-completion" -version = "0.5.2" -description = "Fish, Bash, Zsh and PowerShell completion for Click" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -click = "*" -jinja2 = "*" -shellingham = "*" -six = "*" - [[package]] name = "click-option-group" version = "0.5.3" @@ -1870,14 +1856,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "shellingham" -version = "1.4.0" -description = "Tool to Detect Surrounding Shell" -category = "main" -optional = false -python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,>=2.6" - [[package]] name = "six" version = "1.16.0" @@ -1956,6 +1934,23 @@ sphinx = "*" [package.extras] dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] +[[package]] +name = "sphinx-tabs" +version = "3.2.0" +description = "Tabbed views for Sphinx" +category = "main" +optional = true +python-versions = "~=3.6" + +[package.dependencies] +docutils = ">=0.16.0,<0.17.0" +pygments = "*" +sphinx = ">=2,<5" + +[package.extras] +code_style = ["pre-commit (==2.13.0)"] +testing = ["coverage", "pytest (>=3.6,<4)", "pytest-cov", "pytest-regressions", "pygments", "sphinx-testing", "bs4", "rinohtype"] + [[package]] name = "sphinxcontrib-applehelp" version = "1.0.2" @@ -2415,8 +2410,8 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ cffi = ["cffi (>=1.11)"] [extras] -all = ["apispec", "apispec-webframeworks", "black", "check-manifest", "circus", "coverage", "cwl-utils", "fakeredis", "flakehell", "flaky", "flask", "freezegun", "gunicorn", "isort", "jinja2", "marshmallow", "pexpect", "pillow", "ptvsd", "pydocstyle", "pyte", "pytest", "pytest-black", "pytest-cache", "pytest-cov", "pytest-flake8", "pytest-mock", "pytest-pep8", "pytest-timeout", "pytest-xdist", "python-dotenv", "redis", "renku-sphinx-theme", "responses", "rq", "rq-scheduler", "sentry-sdk", "sphinxcontrib-spelling", "sphinx-rtd-theme", "toil", "walrus"] -docs = ["renku-sphinx-theme", "sphinx-rtd-theme", "sphinxcontrib-spelling"] +all = ["apispec", "apispec-webframeworks", "black", "check-manifest", "circus", "coverage", "cwl-utils", "fakeredis", "flakehell", "flaky", "flask", "freezegun", "gunicorn", "isort", "jinja2", "marshmallow", "pexpect", "pillow", "ptvsd", "pydocstyle", "pyte", "pytest", "pytest-black", "pytest-cache", "pytest-cov", "pytest-flake8", "pytest-mock", "pytest-pep8", "pytest-timeout", "pytest-xdist", "python-dotenv", "redis", "renku-sphinx-theme", "responses", "rq", "rq-scheduler", "sentry-sdk", "sphinxcontrib-spelling", "sphinx-rtd-theme", "sphinx-tabs", "toil", "walrus"] +docs = ["renku-sphinx-theme", "sphinx-rtd-theme", "sphinxcontrib-spelling", "sphinx-tabs"] service = ["apispec", "apispec-webframeworks", "circus", "flask", "gunicorn", "marshmallow", "pillow", "ptvsd", "python-dotenv", "redis", "rq", "rq-scheduler", "sentry-sdk", "walrus"] tests = ["black", "check-manifest", "coverage", "cwl-utils", "fakeredis", "flakehell", "flaky", "freezegun", "isort", "pexpect", "pydocstyle", "pyte", "pytest", "pytest-black", "pytest-cache", "pytest-cov", "pytest-flake8", "pytest-mock", "pytest-pep8", "pytest-timeout", "pytest-xdist", "responses"] toil = ["toil"] @@ -2424,7 +2419,7 @@ toil = ["toil"] [metadata] lock-version = "1.1" python-versions = "^3.7.1" -content-hash = "e257cbf076239a873e3cad21997a98dee0e54f2a0df282a759f381700af4037e" +content-hash = "b801c39914abd9d8632c5df79457f55f36ab7f05af4c00548b60f3fcd40cb502" [metadata.files] addict = [ @@ -2622,9 +2617,6 @@ click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, ] -click-completion = [ - {file = "click-completion-0.5.2.tar.gz", hash = "sha256:5bf816b81367e638a190b6e91b50779007d14301b3f9f3145d68e3cade7bce86"}, -] click-option-group = [ {file = "click-option-group-0.5.3.tar.gz", hash = "sha256:a6e924f3c46b657feb5b72679f7e930f8e5b224b766ab35c91ae4019b4e0615e"}, {file = "click_option_group-0.5.3-py3-none-any.whl", hash = "sha256:9653a2297357335d7325a1827e71ac1245d91c97d959346a7decabd4a52d5354"}, @@ -3726,10 +3718,6 @@ shellescape = [ {file = "shellescape-3.8.1-py2.py3-none-any.whl", hash = "sha256:f17127e390fa3f9aaa80c69c16ea73615fd9b5318fd8309c1dca6168ae7d85bf"}, {file = "shellescape-3.8.1.tar.gz", hash = "sha256:40b310b30479be771bf3ab28bd8d40753778488bd46ea0969ba0b35038c3ec26"}, ] -shellingham = [ - {file = "shellingham-1.4.0-py2.py3-none-any.whl", hash = "sha256:536b67a0697f2e4af32ab176c00a50ac2899c5a05e0d8e2dadac8e58888283f9"}, - {file = "shellingham-1.4.0.tar.gz", hash = "sha256:4855c2458d6904829bd34c299f11fdeed7cfefbf8a2c522e4caea6cd76b3171e"}, -] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -3754,6 +3742,10 @@ sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-0.5.2-py2.py3-none-any.whl", hash = "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f"}, {file = "sphinx_rtd_theme-0.5.2.tar.gz", hash = "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a"}, ] +sphinx-tabs = [ + {file = "sphinx-tabs-3.2.0.tar.gz", hash = "sha256:33137914ed9b276e6a686d7a337310ee77b1dae316fdcbce60476913a152e0a4"}, + {file = "sphinx_tabs-3.2.0-py3-none-any.whl", hash = "sha256:1e1b1846c80137bd81a78e4a69b02664b98b1e1da361beb30600b939dfc75065"}, +] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, diff --git a/pyproject.toml b/pyproject.toml index 79b97d50e8..fb3d495598 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,6 @@ calamus = "<0.4,>=0.3.13" check-manifest = { version = "<0.48,>=0.37", optional = true } circus = { version = "==0.17.1", optional = true } click = "<8.0.2,>=7.0" -click-completion = "<=0.5.3,>=0.5.0" click-option-group = "<0.6.0,>=0.5.2" click-plugins = "==1.1.1" coverage = { version = "<6.2,>=4.5.3", optional = true } @@ -135,6 +134,7 @@ rq-scheduler = { version = "==0.11.0", optional = true } sentry-sdk = { version = "<1.4.4,>=0.7.4", extras = ["flask"], optional = true } sphinxcontrib-spelling = { version = "7.*", optional = true } sphinx-rtd-theme = { version = "<1.1,>=0.5.0", optional = true } +sphinx-tabs = { version = "==3.2.0", optional = true } tabulate = "<0.8.10,>=0.7.7" toil = { version = "==5.5.0", optional = true } tqdm = "<4.62.4,>=4.48.1" @@ -189,7 +189,7 @@ tests = [ "responses", ] toil = ["toil"] -docs = ["renku-sphinx-theme", "sphinx-rtd-theme", "sphinxcontrib-spelling"] +docs = ["renku-sphinx-theme", "sphinx-rtd-theme", "sphinxcontrib-spelling", "sphinx-tabs"] all = [ "apispec", "apispec-webframeworks", @@ -230,6 +230,7 @@ all = [ "sentry-sdk", "sphinxcontrib-spelling", "sphinx-rtd-theme", + "sphinx-tabs", "toil", "walrus", ] diff --git a/renku/cli/__init__.py b/renku/cli/__init__.py index 7dfeb5c202..03e1926577 100644 --- a/renku/cli/__init__.py +++ b/renku/cli/__init__.py @@ -34,7 +34,6 @@ Options: --version Print version number. --global-config-path Print global application's config path. - --install-completion Install completion for the current shell. --path Location of a Renku repository. [default: (dynamic)] --external-storage / -S, --no-external-storage @@ -66,7 +65,6 @@ from pathlib import Path import click -import click_completion import yaml from click_plugins import with_plugins @@ -94,7 +92,7 @@ from renku.cli.update import update from renku.cli.workflow import workflow from renku.core.commands.echo import WARNING -from renku.core.commands.options import install_completion, option_external_storage_requested +from renku.core.commands.options import option_external_storage_requested from renku.core.commands.version import check_version, print_version from renku.core.errors import UsageError from renku.core.utils.git import default_path @@ -116,9 +114,6 @@ def get_entry_points(name: str): return all_entry_points.get(name, []) -#: Monkeypatch Click application. -click_completion.init() - WARNING_UNPROTECTED_COMMANDS = ["clone", "init", "help", "login", "logout", "service", "credentials"] @@ -160,14 +155,6 @@ def is_allowed_command(ctx): is_eager=True, help=print_global_config_path.__doc__, ) -@click.option( - "--install-completion", - is_flag=True, - callback=install_completion, - expose_value=False, - is_eager=True, - help=install_completion.__doc__, -) @click.option( "--path", show_default=True, metavar="", default=default_path, help="Location of a Renku repository." ) diff --git a/renku/cli/dataset.py b/renku/cli/dataset.py index 06a92826a6..d8b405c8d3 100644 --- a/renku/cli/dataset.py +++ b/renku/cli/dataset.py @@ -431,6 +431,16 @@ from renku.core.commands.format.datasets import DATASETS_COLUMNS, DATASETS_FORMATS +def _complete_datasets(ctx, param, incomplete): + from renku.core.commands.dataset import search_datasets + + try: + result = search_datasets().build().execute(name=incomplete) + return result.output + except Exception: + return [] + + @click.group() def dataset(): """Dataset commands.""" @@ -508,7 +518,7 @@ def create(name, title, description, creators, metadata, keyword): @dataset.command() -@click.argument("name") +@click.argument("name", shell_complete=_complete_datasets) @click.option("-t", "--title", default=None, type=click.STRING, help="Title of the dataset.") @click.option("-d", "--description", default=None, type=click.STRING, help="Dataset's description.") @click.option( @@ -570,7 +580,7 @@ def edit(name, title, description, creators, metadata, keyword): @dataset.command("show") -@click.argument("name") +@click.argument("name", shell_complete=_complete_datasets) def show(name): """Show metadata of a dataset.""" from renku.core.commands.dataset import show_dataset @@ -607,8 +617,8 @@ def show(name): @dataset.command() -@click.argument("name") -@click.argument("urls", nargs=-1) +@click.argument("name", shell_complete=_complete_datasets) +@click.argument("urls", type=click.Path(), nargs=-1) @click.option("-e", "--external", is_flag=True, help="Creates a link to external data.") @click.option("--force", is_flag=True, help="Allow adding otherwise ignored files.") @click.option("-o", "--overwrite", is_flag=True, help="Overwrite existing files.") @@ -640,7 +650,7 @@ def add(name, urls, external, force, overwrite, create, sources, destination, re @dataset.command("ls-files") -@click.argument("names", nargs=-1) +@click.argument("names", nargs=-1, shell_complete=_complete_datasets) @click.option( "--creators", help="Filter files which where authored by specific creators. Multiple creators are specified by comma.", @@ -671,7 +681,7 @@ def ls_files(names, creators, include, exclude, format, columns): @dataset.command() -@click.argument("name") +@click.argument("name", shell_complete=_complete_datasets) @click.option("-I", "--include", multiple=True, help="Include files matching given pattern.") @click.option("-X", "--exclude", multiple=True, help="Exclude files matching given pattern.") @click.option("-y", "--yes", is_flag=True, help="Confirm unlinking of all files.") @@ -695,7 +705,7 @@ def remove(name): @dataset.command("tag") -@click.argument("name") +@click.argument("name", shell_complete=_complete_datasets) @click.argument("tag") @click.option("-d", "--description", default="", help="A description for this tag") @click.option("--force", is_flag=True, help="Allow overwriting existing tags.") @@ -708,7 +718,7 @@ def tag(name, tag, description, force): @dataset.command("rm-tags") -@click.argument("name") +@click.argument("name", shell_complete=_complete_datasets) @click.argument("tags", nargs=-1) def remove_tags(name, tags): """Remove tags from a dataset.""" @@ -719,7 +729,7 @@ def remove_tags(name, tags): @dataset.command("ls-tags") -@click.argument("name") +@click.argument("name", shell_complete=_complete_datasets) @click.option("--format", type=click.Choice(DATASET_TAGS_FORMATS), default="tabular", help="Choose an output format.") def ls_tags(name, format): """List all tags of a dataset.""" @@ -776,7 +786,7 @@ def wrapper(f): @dataset.command("export") -@click.argument("name") +@click.argument("name", shell_complete=_complete_datasets) @export_provider_argument() @click.option("-p", "--publish", is_flag=True, help="Automatically publish exported dataset.") @click.option("-t", "--tag", help="Dataset tag to export") @@ -817,7 +827,7 @@ def import_(uri, name, extract, yes): @dataset.command("update") -@click.argument("names", nargs=-1) +@click.argument("names", nargs=-1, shell_complete=_complete_datasets) @click.option( "--creators", help="Filter files which where authored by specific creators. Multiple creators are specified by comma.", diff --git a/renku/cli/workflow.py b/renku/cli/workflow.py index 448a3837ac..f7cb219471 100644 --- a/renku/cli/workflow.py +++ b/renku/cli/workflow.py @@ -558,6 +558,16 @@ from renku.core.commands.view_model.plan import PlanViewModel +def _complete_workflows(ctx, param, incomplete): + from renku.core.commands.workflow import search_workflows_command + + try: + result = search_workflows_command().build().execute(name=incomplete) + return list(filter(lambda x: x.startswith(incomplete), result.output)) + except Exception: + return [] + + def _print_plan(plan: "PlanViewModel"): """Print a plan to stdout.""" from renku.core.utils.os import print_markdown @@ -708,7 +718,7 @@ def list_workflows(format, columns): @workflow.command() -@click.argument("name_or_id", metavar="") +@click.argument("name_or_id", metavar="", shell_complete=_complete_workflows) def show(name_or_id): """Show details for workflow .""" from renku.core.commands.view_model.plan import PlanViewModel @@ -726,7 +736,7 @@ def show(name_or_id): @workflow.command() -@click.argument("name", metavar="") +@click.argument("name", metavar="", shell_complete=_complete_workflows) @click.option("--force", is_flag=True, help="Override the existence check.") def remove(name, force): """Remove a workflow named .""" @@ -762,7 +772,7 @@ def remove(name, force): help="End a composite plan at this file as output.", ) @click.argument("name", required=True) -@click.argument("steps", nargs=-1, type=click.UNPROCESSED) +@click.argument("steps", nargs=-1, type=click.UNPROCESSED, shell_complete=_complete_workflows) def compose( description, mappings, @@ -819,7 +829,7 @@ def compose( @workflow.command() -@click.argument("workflow_name", metavar="") +@click.argument("workflow_name", metavar="", shell_complete=_complete_workflows) @click.option("--name", metavar="", help="New name of the workflow") @click.option("--description", metavar="", help="New description of the workflow") @click.option( @@ -877,7 +887,7 @@ def edit(workflow_name, name, description, set_params, map_params, rename_params @workflow.command() -@click.argument("workflow_name", metavar="") +@click.argument("workflow_name", metavar="", shell_complete=_complete_workflows) @click.option( "--format", default="cwl", @@ -990,7 +1000,7 @@ def outputs(ctx, paths): type=click.Path(exists=True, dir_okay=False), help="YAML file containing parameter mappings to be used.", ) -@click.argument("name_or_id", required=True) +@click.argument("name_or_id", required=True, shell_complete=_complete_workflows) def execute( provider, config, @@ -1127,7 +1137,7 @@ def visualize(sources, columns, exclude_files, ascii, interactive, no_color, pag ) @click.option("mappings", "-m", "--map", multiple=True, help="Mapping for a workflow parameter.") @click.option("config", "-c", "--config", metavar="", help="YAML file containing config for the provider.") -@click.argument("name_or_id", required=True) +@click.argument("name_or_id", required=True, shell_complete=_complete_workflows) def iterate(name_or_id, mappings, mapping_path, dry_run, provider, config): """Execute a workflow by iterating through a range of provided parameters.""" from renku.core.commands.view_model.plan import PlanViewModel diff --git a/renku/core/commands/dataset.py b/renku/core/commands/dataset.py index 7ee2aa20cb..6a849c36c7 100644 --- a/renku/core/commands/dataset.py +++ b/renku/core/commands/dataset.py @@ -58,6 +58,17 @@ from renku.core.utils.urls import remove_credentials +def _search_datasets(name: str) -> List[str]: + """Get all the datasets whose name starts with the given string.""" + datasets_provenance = DatasetsProvenance() + return list(filter(lambda x: x.startswith(name), map(lambda x: x.name, datasets_provenance.datasets))) + + +def search_datasets(): + """Command to get all the datasets whose name starts with the given string.""" + return Command().command(_search_datasets).require_migration().with_database() + + def _list_datasets(format=None, columns=None): """List all datasets.""" datasets_provenance = DatasetsProvenance() diff --git a/renku/core/commands/options.py b/renku/core/commands/options.py index 39bdcac2b7..1383061701 100644 --- a/renku/core/commands/options.py +++ b/renku/core/commands/options.py @@ -21,19 +21,6 @@ from .git import set_git_isolation - -def install_completion(ctx, attr, value): # pragma: no cover - """Install completion for the current shell.""" - import click_completion.core - - if not value or ctx.resilient_parsing: - return value - - shell, path = click_completion.core.install() - click.secho("{0} completion installed in {1}".format(shell, path), fg="green") - ctx.exit() - - option_isolation = click.option( "--isolation", is_flag=True, diff --git a/renku/core/commands/workflow.py b/renku/core/commands/workflow.py index 8af39158a1..b4648bcd1b 100644 --- a/renku/core/commands/workflow.py +++ b/renku/core/commands/workflow.py @@ -73,6 +73,17 @@ def _safe_read_yaml(file: str) -> Dict[str, Any]: raise errors.ParameterError(e) +@inject.autoparams() +def _search_workflows(name: str, plan_gateway: IPlanGateway) -> List[str]: + """Get all the workflows whose Plan.name are greater than or equal to the given name.""" + return plan_gateway.list_by_name(starts_with=name) + + +def search_workflows_command(): + """Command to get all the workflows whose Plan.name are greater than or equal to the given name.""" + return Command().command(_search_workflows).require_migration().with_database(write=False) + + @inject.autoparams() def _find_workflow(name_or_id: str, plan_gateway: IPlanGateway) -> AbstractPlan: workflow = plan_gateway.get_by_id(name_or_id) or plan_gateway.get_by_name(name_or_id) diff --git a/renku/core/management/interface/plan_gateway.py b/renku/core/management/interface/plan_gateway.py index 71381874c6..e6f23b4d3e 100644 --- a/renku/core/management/interface/plan_gateway.py +++ b/renku/core/management/interface/plan_gateway.py @@ -34,6 +34,10 @@ def get_by_name(self, name: str) -> Optional[AbstractPlan]: """Get a plan by name.""" raise NotImplementedError + def list_by_name(self, starts_with: str, ends_with: str = None) -> List[str]: + """Search plans by name.""" + raise NotImplementedError + def get_newest_plans_by_names(self, with_invalidated: bool = False) -> Dict[str, AbstractPlan]: """Return a list of all newest plans with their names.""" raise NotImplementedError diff --git a/renku/core/metadata/database.py b/renku/core/metadata/database.py index 0f200fbf36..73713551e8 100644 --- a/renku/core/metadata/database.py +++ b/renku/core/metadata/database.py @@ -530,9 +530,9 @@ def pop(self, key, default=MARKER): return return self._entries.pop(key) if default is MARKER else self._entries.pop(key, default) - def keys(self): + def keys(self, min=None, max=None, excludemin=False, excludemax=False): """Return an iterator of keys.""" - return self._entries.keys() + return self._entries.keys(min=min, max=max, excludemin=excludemin, excludemax=excludemax) def values(self): """Return an iterator of values.""" diff --git a/renku/core/metadata/gateway/plan_gateway.py b/renku/core/metadata/gateway/plan_gateway.py index 8c3935e6c5..598e91434a 100644 --- a/renku/core/metadata/gateway/plan_gateway.py +++ b/renku/core/metadata/gateway/plan_gateway.py @@ -38,6 +38,10 @@ def get_by_name(self, name: str) -> Optional[AbstractPlan]: """Get a plan by name.""" return self.database_dispatcher.current_database["plans-by-name"].get(name) + def list_by_name(self, starts_with: str, ends_with: str = None) -> List[str]: + """Search plans by name.""" + return self.database_dispatcher.current_database["plans-by-name"].keys(min=starts_with, max=ends_with) + def get_newest_plans_by_names(self, with_invalidated: bool = False) -> Dict[str, AbstractPlan]: """Return a list of all newest plans with their names.""" database = self.database_dispatcher.current_database