From 86d99df140f5072b0c77d2dc7c0cb9cffd65d282 Mon Sep 17 00:00:00 2001 From: "TEAM 4.0[bot]" Date: Thu, 21 Nov 2024 13:03:25 +0100 Subject: [PATCH] [Auto-generated] Update dependencies (#214) * Update dependencies in `pyproject.toml` (#206, #209, #213) * Bump codecov/codecov-action from 4 to 5 (#208) Remove deprecated typer options. Manually sort commands for CLI docs. Update CLI docs according to updated typer generation-algorithm. Add timeout for `build_cli_docs.sh` file. Signed-off-by: dependabot[bot] Co-authored-by: Casper Welzel Andersen Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci_tests.yml | 4 +- build_cli_docs.sh | 10 +- docs/CLI.md | 130 ++++++++-------- entities_service/cli/commands/__init__.py | 4 +- entities_service/cli/commands/config.py | 83 +++++----- entities_service/cli/commands/list.py | 182 +++++++++++----------- pyproject.toml | 10 +- 7 files changed, 215 insertions(+), 208 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 92fb4ba..7c8b5ea 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -207,7 +207,7 @@ jobs: - name: Upload coverage if: github.repository_owner == 'SINTEF' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -254,7 +254,7 @@ jobs: - name: Upload coverage if: github.repository_owner == 'SINTEF' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/build_cli_docs.sh b/build_cli_docs.sh index 8435d4a..cf1accc 100755 --- a/build_cli_docs.sh +++ b/build_cli_docs.sh @@ -17,12 +17,20 @@ fi rm /tmp/CLI.md 2> /dev/null +TIMEOUT_EXECUTION_SECONDS=5 +SECONDS=0 + # Generate CLI documentation typer entities_service.cli.main utils docs --output /tmp/CLI.md > /dev/null -until grep -q "# \`entities-service\`" /tmp/CLI.md; do +until grep -q "# \`entities-service\`" /tmp/CLI.md || [ "$SECONDS" -gt "${TIMEOUT_EXECUTION_SECONDS}" ]; do typer entities_service.cli.main utils docs --output /tmp/CLI.md > /dev/null done +if [ "$SECONDS" -gt "${TIMEOUT_EXECUTION_SECONDS}" ]; then + echo "❌ Timeout while generating CLI documentation." + exit 1 +fi + diff --suppress-common-lines /tmp/CLI.md ${DOCS_PATH} > /dev/null 2>&1 ERROR_CODE=$? diff --git a/docs/CLI.md b/docs/CLI.md index 93e737d..f8e8cd5 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -18,11 +18,73 @@ $ entities-service [OPTIONS] COMMAND [ARGS]... **Commands**: -* `config`: Manage configuration options. -* `list`: List resources. * `login`: Login to the entities service. * `upload`: Upload (local) entities to a remote location. * `validate`: Validate (local) entities. +* `config`: Manage configuration options. +* `list`: List resources. + +## `entities-service login` + +Login to the entities service. + +**Usage**: + +```console +$ entities-service login [OPTIONS] +``` + +**Options**: + +* `-q, -s, -y, --quiet, --silent`: Do not print anything on success and do not ask for confirmation. +* `--help`: Show this message and exit. + +## `entities-service upload` + +Upload (local) entities to a remote location. + +**Usage**: + +```console +$ entities-service upload [OPTIONS] [SOURCE]... +``` + +**Arguments**: + +* `[SOURCE]...`: Path to file or directory with one or more entities. + +**Options**: + +* `--format [json|yaml|yml]`: Format of entity file(s). [default: json] +* `--fail-fast`: Stop uploading entities on the first error during file validation. +* `-q, -s, --quiet, --silent`: Do not print anything on success and do not ask for confirmation. IMPORTANT, for content conflicts the defaults will be chosen. +* `-y, --auto-confirm`: Automatically agree to any confirmations and use defaults for content conflicts. This differs from --quiet in that it will still print information. +* `--strict`: Strict validation of entities. This means the command will fail during the validation process, if an external entity already exists and the two entities are not equal. This option is only relevant if '--no-external-calls' is not provided. If both '--no-external-calls' and this options is provided, an error will be emitted. +* `--help`: Show this message and exit. + +## `entities-service validate` + +Validate (local) entities. + +**Usage**: + +```console +$ entities-service validate [OPTIONS] [SOURCE]... +``` + +**Arguments**: + +* `[SOURCE]...`: Path to file or directory with one or more entities. + +**Options**: + +* `--format [json|yaml|yml]`: Format of entity file(s). [default: json] +* `--fail-fast`: Stop validating entities on the first discovered error. +* `-q, -s, -y, --quiet, --silent`: Do not print anything on success. +* `--no-external-calls`: Do not make any external calls to validate the entities. This includes mainly comparing local entities with their remote counterparts. +* `-v, --verbose`: Print the differences between the external and local entities (if any). +* `--strict`: Strict validation of entities. This means validation will fail if an external entity already exists and the two entities are not equal. This option is only relevant if '--no-external-calls' is not provided. If both '--no-external-calls' and this options is provided, an error will be emitted. +* `--help`: Show this message and exit. ## `entities-service config` @@ -59,7 +121,7 @@ $ entities-service config set [OPTIONS] KEY:{access_token|backend|base_url|ca_fi **Arguments**: -* `KEY:{access_token|backend|base_url|ca_file|debug|mongo_collection|mongo_db|mongo_password|mongo_uri|mongo_user|oauth2_provider|oauth2_provider_base_url|roles_group|x509_certificate_file}`: Configuration option to set. These can also be set as an environment variable by prefixing with 'ENTITIES_SERVICE_'. [required] +* `KEY:{access_token|backend|base_url|ca_file|debug|mongo_collection|mongo_db|mongo_password|mongo_uri|mongo_user|oauth2_provider|oauth2_provider_base_url|roles_group|x509_certificate_file}`: Configuration option to set. These can also be set as an environment variable by prefixing with 'ENTITIES_SERVICE_'. [required] * `[VALUE]`: Value to set. This will be prompted for if not provided. **Options**: @@ -164,65 +226,3 @@ $ entities-service list namespaces [OPTIONS] **Options**: * `--help`: Show this message and exit. - -## `entities-service login` - -Login to the entities service. - -**Usage**: - -```console -$ entities-service login [OPTIONS] -``` - -**Options**: - -* `-q, -s, -y, --quiet, --silent`: Do not print anything on success and do not ask for confirmation. -* `--help`: Show this message and exit. - -## `entities-service upload` - -Upload (local) entities to a remote location. - -**Usage**: - -```console -$ entities-service upload [OPTIONS] [SOURCE]... -``` - -**Arguments**: - -* `[SOURCE]...`: Path to file or directory with one or more entities. - -**Options**: - -* `--format [json|yaml|yml]`: Format of entity file(s). [default: json] -* `--fail-fast`: Stop uploading entities on the first error during file validation. -* `-q, -s, --quiet, --silent`: Do not print anything on success and do not ask for confirmation. IMPORTANT, for content conflicts the defaults will be chosen. -* `-y, --auto-confirm`: Automatically agree to any confirmations and use defaults for content conflicts. This differs from --quiet in that it will still print information. -* `--strict`: Strict validation of entities. This means the command will fail during the validation process, if an external entity already exists and the two entities are not equal. This option is only relevant if '--no-external-calls' is not provided. If both '--no-external-calls' and this options is provided, an error will be emitted. -* `--help`: Show this message and exit. - -## `entities-service validate` - -Validate (local) entities. - -**Usage**: - -```console -$ entities-service validate [OPTIONS] [SOURCE]... -``` - -**Arguments**: - -* `[SOURCE]...`: Path to file or directory with one or more entities. - -**Options**: - -* `--format [json|yaml|yml]`: Format of entity file(s). [default: json] -* `--fail-fast`: Stop validating entities on the first discovered error. -* `-q, -s, -y, --quiet, --silent`: Do not print anything on success. -* `--no-external-calls`: Do not make any external calls to validate the entities. This includes mainly comparing local entities with their remote counterparts. -* `-v, --verbose`: Print the differences between the external and local entities (if any). -* `--strict`: Strict validation of entities. This means validation will fail if an external entity already exists and the two entities are not equal. This option is only relevant if '--no-external-calls' is not provided. If both '--no-external-calls' and this options is provided, an error will be emitted. -* `--help`: Show this message and exit. diff --git a/entities_service/cli/commands/__init__.py b/entities_service/cli/commands/__init__.py index 4106acf..8395a2d 100644 --- a/entities_service/cli/commands/__init__.py +++ b/entities_service/cli/commands/__init__.py @@ -31,7 +31,7 @@ def get_commands() -> Generator[tuple[Callable, dict[str, Any]]]: """ this_dir = Path(__file__).parent.resolve() - for path in this_dir.glob("*.py"): + for path in sorted(this_dir.glob("*.py")): if path.stem in ("__init__", *SUB_TYPER_APPS): continue @@ -63,7 +63,7 @@ def get_subtyper_apps() -> Generator[tuple[Typer, dict[str, Any]]]: """ this_dir = Path(__file__).parent.resolve() - for path in this_dir.glob("*.py"): + for path in sorted(this_dir.glob("*.py")): if path.stem not in SUB_TYPER_APPS: continue diff --git a/entities_service/cli/commands/config.py b/entities_service/cli/commands/config.py index 7fd64de..2d9dd10 100644 --- a/entities_service/cli/commands/config.py +++ b/entities_service/cli/commands/config.py @@ -142,47 +142,6 @@ def set_config( ) -@APP.command() -def unset( - key: Annotated[ - ConfigFields, - typer.Argument( - help="Configuration option to unset.", - show_choices=True, - shell_complete=ConfigFields.autocomplete, - case_sensitive=False, - show_default=False, - ), - ], -) -> None: - """Unset a single configuration option.""" - dotenv_file = CONTEXT["dotenv_path"] - - if dotenv_file.exists(): - unset_key(dotenv_file, f"{CONFIG.model_config['env_prefix']}{key}".upper()) - print(f"Unset {CONFIG.model_config['env_prefix'].upper()}{key.upper()}.") - else: - print(f"{dotenv_file} file not found.") - - -@APP.command() -def unset_all() -> None: - """Unset all configuration options.""" - dotenv_file = CONTEXT["dotenv_path"] - - typer.confirm( - "Are you sure you want to unset (remove) all configuration options in " - f"{dotenv_file} file, deleting the file in the process?", - abort=True, - ) - - if dotenv_file.exists(): - dotenv_file.unlink() - print(f"Unset all configuration options. (Removed {dotenv_file}.)") - else: - print(f"{dotenv_file} file not found.") - - @APP.command() def show( reveal_sensitive: Annotated[ @@ -190,7 +149,6 @@ def show( typer.Option( "--reveal-sensitive", help="Reveal sensitive values. (DANGEROUS! Use with caution.)", - is_flag=True, show_default=False, ), ] = False, @@ -230,3 +188,44 @@ def show( for key, value in output.items() ) ) + + +@APP.command() +def unset( + key: Annotated[ + ConfigFields, + typer.Argument( + help="Configuration option to unset.", + show_choices=True, + shell_complete=ConfigFields.autocomplete, + case_sensitive=False, + show_default=False, + ), + ], +) -> None: + """Unset a single configuration option.""" + dotenv_file = CONTEXT["dotenv_path"] + + if dotenv_file.exists(): + unset_key(dotenv_file, f"{CONFIG.model_config['env_prefix']}{key}".upper()) + print(f"Unset {CONFIG.model_config['env_prefix'].upper()}{key.upper()}.") + else: + print(f"{dotenv_file} file not found.") + + +@APP.command() +def unset_all() -> None: + """Unset all configuration options.""" + dotenv_file = CONTEXT["dotenv_path"] + + typer.confirm( + "Are you sure you want to unset (remove) all configuration options in " + f"{dotenv_file} file, deleting the file in the process?", + abort=True, + ) + + if dotenv_file.exists(): + dotenv_file.unlink() + print(f"Unset all configuration options. (Removed {dotenv_file}.)") + else: + print(f"{dotenv_file} file not found.") diff --git a/entities_service/cli/commands/list.py b/entities_service/cli/commands/list.py index 1e34271..822426c 100644 --- a/entities_service/cli/commands/list.py +++ b/entities_service/cli/commands/list.py @@ -40,97 +40,6 @@ ) -@APP.command() -def namespaces( - # Hidden options - used only when calling the function directly - return_info: Annotated[ - bool, - typer.Option( - hidden=True, - help=( - "Avoid printing the namespaces and instead return them as a Python " - "list. Useful when calling this function from another function." - ), - ), - ] = False, -) -> list[str] | None: - """List namespaces from the entities service.""" - with httpx.Client(base_url=str(CONFIG.base_url), timeout=10) as client: - try: - response = client.get("/_api/namespaces") - except httpx.HTTPError as exc: - ERROR_CONSOLE.print( - "[bold red]Error[/bold red]: Could not list namespaces. HTTP " - f"exception: {exc}" - ) - raise typer.Exit(1) from exc - - # Decode response - try: - namespaces: dict[str, Any] | list[str] = response.json() - except json.JSONDecodeError as exc: - ERROR_CONSOLE.print( - f"[bold red]Error[/bold red]: Could not list namespaces. JSON decode " - f"error: {exc}" - ) - raise typer.Exit(1) from exc - - # Unsuccessful response (!= 200 OK) - if not response.is_success: - # First, it may be that there are no namespaces - if ( - response.status_code == 500 - and isinstance(namespaces, dict) - and namespaces.get("detail") == "No namespaces found in the backend." - ): - print( - "[bold yellow]Warning[/bold yellow]: No namespaces found. There are no " - f"entities hosted. Ensure {CONFIG.base_url} is the desired service to " - "target." - ) - namespaces = [] - - # Or it may be an error - else: - ERROR_CONSOLE.print( - f"[bold red]Error[/bold red]: Could not list namespaces. HTTP status " - f"code: {response.status_code}. Error response: " - ) - ERROR_CONSOLE.print_json(data=namespaces) - raise typer.Exit(1) - - # Bad response format - if not isinstance(namespaces, list): - # Expect a list of namespaces - ERROR_CONSOLE.print( - f"[bold red]Error[/bold red]: Could not list namespaces. Invalid response: " - f"{namespaces}" - ) - raise typer.Exit(1) - - if return_info: - return namespaces - - if not namespaces: - raise typer.Exit() - - # Print namespaces - table = Table( - box=box.HORIZONTALS, - show_edge=False, - highlight=True, - ) - - table.add_column("Namespaces:", no_wrap=True) - - for namespace in sorted(namespaces): - table.add_row(namespace) - - print("", table, "") - - return None - - @APP.command() def entities( namespace: Annotated[ @@ -287,6 +196,97 @@ def entities( print(f"\nBase namespace: {core_namespace}\n{single_namespace}", table, "") +@APP.command() +def namespaces( + # Hidden options - used only when calling the function directly + return_info: Annotated[ + bool, + typer.Option( + hidden=True, + help=( + "Avoid printing the namespaces and instead return them as a Python " + "list. Useful when calling this function from another function." + ), + ), + ] = False, +) -> list[str] | None: + """List namespaces from the entities service.""" + with httpx.Client(base_url=str(CONFIG.base_url), timeout=10) as client: + try: + response = client.get("/_api/namespaces") + except httpx.HTTPError as exc: + ERROR_CONSOLE.print( + "[bold red]Error[/bold red]: Could not list namespaces. HTTP " + f"exception: {exc}" + ) + raise typer.Exit(1) from exc + + # Decode response + try: + namespaces: dict[str, Any] | list[str] = response.json() + except json.JSONDecodeError as exc: + ERROR_CONSOLE.print( + f"[bold red]Error[/bold red]: Could not list namespaces. JSON decode " + f"error: {exc}" + ) + raise typer.Exit(1) from exc + + # Unsuccessful response (!= 200 OK) + if not response.is_success: + # First, it may be that there are no namespaces + if ( + response.status_code == 500 + and isinstance(namespaces, dict) + and namespaces.get("detail") == "No namespaces found in the backend." + ): + print( + "[bold yellow]Warning[/bold yellow]: No namespaces found. There are no " + f"entities hosted. Ensure {CONFIG.base_url} is the desired service to " + "target." + ) + namespaces = [] + + # Or it may be an error + else: + ERROR_CONSOLE.print( + f"[bold red]Error[/bold red]: Could not list namespaces. HTTP status " + f"code: {response.status_code}. Error response: " + ) + ERROR_CONSOLE.print_json(data=namespaces) + raise typer.Exit(1) + + # Bad response format + if not isinstance(namespaces, list): + # Expect a list of namespaces + ERROR_CONSOLE.print( + f"[bold red]Error[/bold red]: Could not list namespaces. Invalid response: " + f"{namespaces}" + ) + raise typer.Exit(1) + + if return_info: + return namespaces + + if not namespaces: + raise typer.Exit() + + # Print namespaces + table = Table( + box=box.HORIZONTALS, + show_edge=False, + highlight=True, + ) + + table.add_column("Namespaces:", no_wrap=True) + + for namespace in sorted(namespaces): + table.add_row(namespace) + + print("", table, "") + + return None + + def _parse_namespace(namespace: str | None, allow_external: bool = True) -> str: """Parse a (specific) namespace, returning a full namespace.""" # If a full URI (including version and name) is passed, diff --git a/pyproject.toml b/pyproject.toml index d53d035..a7ea663 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,28 +42,28 @@ requires-python = "~=3.10" dynamic = ["version", "description"] dependencies = [ - "fastapi ~=0.115.4", + "fastapi ~=0.115.5", "httpx ~=0.27.2", "pydantic-settings ~=2.6", "pymongo ~=4.10", "python-dotenv ~=1.0", - "uvicorn ~=0.32.0", + "uvicorn ~=0.32.1", ] [project.optional-dependencies] cli = [ "httpx-auth ~=0.22.0", "pyyaml ~=6.0", - "typer ~=0.12.5", + "typer ~=0.13.1", ] testing = [ "cryptography ~=43.0", "dlite-python ~=0.5.23", - "mongomock ~=4.2", + "mongomock ~=4.3", "pytest ~=8.3", "pytest-asyncio ~=0.24.0", "pytest-cov ~=6.0", - "pytest-httpx ~=0.33.0", + "pytest-httpx ~=0.34.0", "entities-service[cli]", ] server = [