From 29d969ca8e6174c3c262df6c7ff42fa8e6770632 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Tue, 10 Sep 2024 15:44:11 +0200 Subject: [PATCH] feat!: rename subcommands Do not use plurals for the subcommands. --- src/ai4_cli/cli/__init__.py | 4 +- src/ai4_cli/cli/tools.py | 151 ++++++++++++++++++++++++++++++++++ src/ai4_cli/client/tools.py | 25 ++++++ src/ai4_cli/tests/test_cli.py | 26 ++++-- 4 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 src/ai4_cli/cli/tools.py create mode 100644 src/ai4_cli/client/tools.py diff --git a/src/ai4_cli/cli/__init__.py b/src/ai4_cli/cli/__init__.py index 6d9244e..5a6e45c 100644 --- a/src/ai4_cli/cli/__init__.py +++ b/src/ai4_cli/cli/__init__.py @@ -28,8 +28,8 @@ the DOTENV_FILE environment variable. """ ) -app.add_typer(modules.app, name="modules") -app.add_typer(tools.app, name="tools") +app.add_typer(modules.app, name="module") +app.add_typer(tools.app, name="tool") DOTENV_FILE = os.getenv("AI4_DOTENV_FILE", ".env.ai4") diff --git a/src/ai4_cli/cli/tools.py b/src/ai4_cli/cli/tools.py new file mode 100644 index 0000000..d608041 --- /dev/null +++ b/src/ai4_cli/cli/tools.py @@ -0,0 +1,151 @@ +"""Handle CLI commands for tools.""" + +import enum +from typing_extensions import Annotated +from typing import List, Optional + +import typer + +from ai4_cli.client import client +from ai4_cli import exceptions +from ai4_cli import utils + +app = typer.Typer(help="List and get details of the defined tools.") + + +class ToolColumns(str, enum.Enum): + """Columns to show in the list command.""" + + ID = "ID" + NAME = "Tool name" + SUMMARY = "Summary" + KEYWORDS = "Keywords" + + +@app.command(name="list") +def list( + ctx: typer.Context, + long: Annotated[ + bool, + typer.Option( + "--long", + "-l", + help="Show more details.", + ), + ] = False, + sort: Annotated[ + ToolColumns, + typer.Option( + "--sort", + help="Sort the tools by the given field.", + ), + ] = ToolColumns.ID, + tags: Annotated[ + Optional[List[str]], + typer.Option( + "--tags", + help="Filter tools by tags. The given tags must all be present on a " + "module to be included in the results. Boolean expression is " + "t1 AND t2.", + ), + ] = None, + not_tags: Annotated[ + Optional[List[str]], + typer.Option( + "--not-tags", + help="Filter tools by tags. Only the tools that do not have any of the " + "given tags will be included in the results. Boolean expression is " + "NOT (t1 AND t2).", + ), + ] = None, + tags_any: Annotated[ + Optional[List[str]], + typer.Option( + "--tags-any", + help="Filter tools by tags. If any of the given tags is present on a " + "module it will be included in the results. Boolean expression is " + "t1 OR t2.", + ), + ] = None, + not_tags_any: Annotated[ + Optional[List[str]], + typer.Option( + "--not-tags-any", + help="Filter tools by tags. Only the tools that do not have at least " + "any of the given tags will be included in the results. " + "Boolean expression is " + "NOT (t1 OR t2).", + ), + ] = None, +): + """List all tools.""" + endpoint = ctx.obj.endpoint + version = ctx.obj.api_version + debug = ctx.obj.debug + + cli = client.AI4Client(endpoint, version, http_debug=debug) + filters = { + "tags": tags, + "not_tags": not_tags, + "tags_any": tags_any, + "not_tags_any": not_tags_any, + } + _, content = cli.tools.list(filters=filters) + + if long: + rows = [ + [ + k.get("name"), + k.get("title"), + k.get("summary"), + ", ".join(k.get("keywords")), + ] + for k in content + ] + + columns = [ + ToolColumns.ID, + ToolColumns.NAME, + ToolColumns.SUMMARY, + ToolColumns.KEYWORDS, + ] + else: + rows = [[k.get("name"), k.get("title"), k.get("summary")] for k in content] + columns = [ + ToolColumns.ID, + ToolColumns.NAME, + ToolColumns.SUMMARY, + ] + + try: + idx = columns.index(sort) + except ValueError: + e = exceptions.InvalidUsageError(f"Invalid column to sort by: {sort}") + utils.format_rich_error(e) + raise typer.Exit() + + sorted_rows = sorted(rows, key=lambda x: x[idx]) + utils.format_list( + columns=columns, + items=sorted_rows, + ) + + +@app.command(name="show") +def show( + ctx: typer.Context, + module_id: str = typer.Argument(..., help="The ID of the module to show."), +): + """Show details of a module.""" + endpoint = ctx.obj.endpoint + version = ctx.obj.api_version + debug = ctx.obj.debug + + cli = client.AI4Client(endpoint, version, http_debug=debug) + try: + _, content = cli.tools.show(module_id) + except exceptions.BaseHTTPError as e: + utils.format_rich_error(e) + raise typer.Exit() + + utils.format_dict(content, exclude=["tosca", "continuous_integration"]) diff --git a/src/ai4_cli/client/tools.py b/src/ai4_cli/client/tools.py new file mode 100644 index 0000000..435100e --- /dev/null +++ b/src/ai4_cli/client/tools.py @@ -0,0 +1,25 @@ +"""Tools (catalog) HTTP client.""" + + +class _Tools(object): + """Tools HTTP client.""" + + def __init__(self, client): + """Create a new instance. + + :param client: The AI4Client instance. + """ + self.client = client + + def list(self, filters=None): + """List all tools.""" + params = {} + for key, value in filters.items(): + if value is None: + continue + params[key] = value + return self.client.request("catalog/tools/detail", "GET", params=params) + + def show(self, tool_id): + """Show details of a tool.""" + return self.client.request(f"catalog/tools/{tool_id}/metadata", "GET") diff --git a/src/ai4_cli/tests/test_cli.py b/src/ai4_cli/tests/test_cli.py index 80abcdb..40578d1 100644 --- a/src/ai4_cli/tests/test_cli.py +++ b/src/ai4_cli/tests/test_cli.py @@ -13,17 +13,33 @@ def test_version(): assert ai4_cli.extract_version() in result.output -def test_modules_command(): +def test_module_command(): """Test that the modules command is available.""" - result = typer.testing.CliRunner().invoke(cli.app, ["modules", "--help"]) + result = typer.testing.CliRunner().invoke(cli.app, ["module", "--help"]) assert result.exit_code == 0 - assert "List and get details of the defined modules and tools." in result.output + assert "List and get details of the defined modules." in result.output -def test_modules_list_and_wrong_api_version(): +def test_module_list_and_wrong_api_version(): """Test that the modules list command fails with an invalid API version.""" result = typer.testing.CliRunner().invoke( - cli.app, ["--api-version", "v2", "modules", "list"] + cli.app, ["--api-version", "v2", "module", "list"] + ) + assert result.exit_code == 2 + assert "Invalid value for '--api-version'" in result.output + + +def test_tool_command(): + """Test that the tools command is available.""" + result = typer.testing.CliRunner().invoke(cli.app, ["tool", "--help"]) + assert result.exit_code == 0 + assert "List and get details of the defined tools." in result.output + + +def test_tool_list_and_wrong_api_version(): + """Test that the tools list command fails with an invalid API version.""" + result = typer.testing.CliRunner().invoke( + cli.app, ["--api-version", "v2", "tool", "list"] ) assert result.exit_code == 2 assert "Invalid value for '--api-version'" in result.output