From a08710f572cacec9c19bb047f9d0a51c6de882ac Mon Sep 17 00:00:00 2001 From: "omer.roth" Date: Wed, 7 Oct 2026 17:17:34 +0300 Subject: [PATCH 1/2] CM-74534: Fix vulnerable dependencies and drop Python 3.9 support Upgrade dependencies flagged by Dependabot alerts and consolidate the open Dependabot PRs (#555, #557, #558, #559, #560, #561, #563): - pyjwt 2.15.1, urllib3 2.8.0, anyio 4.15.1, cryptography 50.0.1, marshmallow 4.3.1, requests 2.34.2, setuptools 83.0.0 - pytest 9.0.3, responses 0.26.3, ruff 0.16.9 - docker/setup-qemu-action v4.4.0, docker/setup-buildx-action v4.4.1, docker/build-push-action v7.4.0 Most patched releases require Python 3.10+, so the minimum supported version is now 3.10: - requires-python >=3.10, classifiers and CI matrices updated - drop Python version markers on mcp, truststore, pywin32, anyio - mcp command and truststore are always available; remove the 3.9 fallbacks in trust_store, host_info, app.py and pyinstaller.spec - ruff targets py310: Optional/Union rewritten to PEP 604 unions Also fix new ruff 0.16 findings and a test JWT fixture whose signature segment was not valid base64url (pyjwt 2.14+ decodes it even when verification is disabled). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docker-image.yml | 10 +- .github/workflows/pre_release.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/ruff.yml | 2 +- .github/workflows/tests.yml | 2 +- .github/workflows/tests_full.yml | 2 +- CONTRIBUTING.md | 2 +- README.md | 10 +- cycode/cli/app.py | 19 +- cycode/cli/apps/activation_manager.py | 12 +- .../cli/apps/ai_guardrails/command_utils.py | 3 +- .../cli/apps/ai_guardrails/hooks_manager.py | 13 +- .../apps/ai_guardrails/ides/_plugin_utils.py | 11 +- .../apps/ai_guardrails/ides/_skill_utils.py | 3 +- cycode/cli/apps/ai_guardrails/ides/base.py | 20 +- .../apps/ai_guardrails/ides/claude_code.py | 28 +- cycode/cli/apps/ai_guardrails/ides/codex.py | 22 +- cycode/cli/apps/ai_guardrails/ides/copilot.py | 14 +- cycode/cli/apps/ai_guardrails/ides/cursor.py | 10 +- .../cli/apps/ai_guardrails/install_command.py | 6 +- .../ai_guardrails/scan/guardrail_config.py | 21 +- .../cli/apps/ai_guardrails/scan/handlers.py | 29 +- .../ai_guardrails/scan/mcp_server_status.py | 12 +- cycode/cli/apps/ai_guardrails/scan/payload.py | 27 +- cycode/cli/apps/ai_guardrails/scan/policy.py | 6 +- .../apps/ai_guardrails/scan/scan_command.py | 8 +- .../ai_guardrails/session_start_command.py | 16 +- .../cli/apps/ai_guardrails/status_command.py | 4 +- .../apps/ai_guardrails/uninstall_command.py | 4 +- cycode/cli/apps/api/__init__.py | 6 +- cycode/cli/apps/api/api_command.py | 12 +- cycode/cli/apps/api/openapi_spec.py | 9 +- cycode/cli/apps/auth/auth_common.py | 8 +- .../cli/apps/configure/configure_command.py | 6 +- cycode/cli/apps/configure/messages.py | 6 +- cycode/cli/apps/configure/prompts.py | 12 +- cycode/cli/apps/ignore/ignore_command.py | 14 +- cycode/cli/apps/mcp/mcp_command.py | 22 +- cycode/cli/apps/report/sbom/common.py | 10 +- cycode/cli/apps/report/sbom/sbom_command.py | 4 +- .../cli/apps/report/sbom/sbom_report_file.py | 3 +- .../apps/report_import/sbom/sbom_command.py | 6 +- cycode/cli/apps/sca_options.py | 6 +- cycode/cli/apps/scan/aggregation_report.py | 8 +- cycode/cli/apps/scan/code_scanner.py | 15 +- cycode/cli/apps/scan/commit_range_scanner.py | 20 +- cycode/cli/apps/scan/detection_excluder.py | 6 +- .../scan/pre_commit/pre_commit_command.py | 6 +- .../apps/scan/pre_push/pre_push_command.py | 6 +- .../scan/pre_receive/pre_receive_command.py | 4 +- cycode/cli/apps/scan/remote_url_resolver.py | 14 +- .../scan/repository/repository_command.py | 4 +- cycode/cli/apps/scan/scan_command.py | 4 +- cycode/cli/apps/scan/scan_parameters.py | 4 +- cycode/cli/apps/scan/scan_result.py | 10 +- cycode/cli/console.py | 4 +- cycode/cli/exceptions/custom_exceptions.py | 2 +- cycode/cli/exceptions/handle_errors.py | 4 +- cycode/cli/exceptions/handle_scan_errors.py | 3 +- .../files_collector/commit_range_documents.py | 28 +- .../files_collector/models/in_memory_zip.py | 6 +- .../files_collector/repository_documents.py | 6 +- .../sca/base_restore_dependencies.py | 15 +- .../sca/go/restore_go_dependencies.py | 3 +- .../sca/maven/restore_gradle_dependencies.py | 5 +- .../sca/maven/restore_maven_dependencies.py | 7 +- .../sca/npm/restore_bun_dependencies.py | 7 +- .../sca/npm/restore_deno_dependencies.py | 3 +- .../sca/npm/restore_pnpm_dependencies.py | 5 +- .../sca/npm/restore_yarn_dependencies.py | 5 +- .../sca/npm/workspace/coverage.py | 10 +- .../sca/npm/workspace/files.py | 5 +- .../sca/npm/workspace/globs.py | 3 +- .../sca/npm/workspace/resolvers.py | 30 +- .../sca/php/restore_composer_dependencies.py | 3 +- .../sca/python/restore_pip_dependencies.py | 5 +- .../sca/python/restore_pipenv_dependencies.py | 3 +- .../sca/python/restore_poetry_dependencies.py | 5 +- .../sca/python/restore_uv_dependencies.py | 5 +- .../files_collector/sca/sca_file_collector.py | 10 +- cycode/cli/files_collector/zip_documents.py | 3 +- cycode/cli/models.py | 16 +- cycode/cli/printers/console_printer.py | 18 +- cycode/cli/printers/json_printer.py | 4 +- cycode/cli/printers/printer_base.py | 6 +- cycode/cli/printers/rich_printer.py | 4 +- cycode/cli/printers/tables/table.py | 8 +- cycode/cli/printers/tables/table_models.py | 4 +- .../cli/printers/tables/table_printer_base.py | 4 +- cycode/cli/printers/text_printer.py | 8 +- .../cli/printers/utils/code_snippet_syntax.py | 2 +- cycode/cli/printers/utils/detection_data.py | 6 +- cycode/cli/printers/utils/sca_ossf.py | 8 +- .../cli/printers/utils/sca_policy_details.py | 3 +- .../cli/user_settings/config_file_manager.py | 20 +- .../user_settings/configuration_manager.py | 12 +- .../cli/user_settings/credentials_manager.py | 11 +- cycode/cli/utils/get_api_client.py | 10 +- cycode/cli/utils/git_proxy.py | 16 +- cycode/cli/utils/host_info.py | 59 +-- cycode/cli/utils/ignore_utils.py | 28 +- cycode/cli/utils/jwt_utils.py | 6 +- cycode/cli/utils/path_utils.py | 12 +- cycode/cli/utils/progress_bar.py | 8 +- cycode/cli/utils/scan_batch.py | 3 +- cycode/cli/utils/scan_utils.py | 4 +- cycode/cli/utils/shell_executor.py | 7 +- cycode/cli/utils/task_timer.py | 8 +- cycode/cli/utils/trust_store.py | 17 - cycode/cli/utils/url_utils.py | 3 +- cycode/cli/utils/version_checker.py | 9 +- cycode/cli/utils/yaml_utils.py | 2 +- cycode/config.py | 3 +- cycode/cyclient/ai_security_manager_client.py | 34 +- cycode/cyclient/auth_client.py | 6 +- cycode/cyclient/base_token_auth_client.py | 4 +- cycode/cyclient/client_creator.py | 10 +- cycode/cyclient/cycode_client_base.py | 19 +- cycode/cyclient/cycode_dev_based_client.py | 4 +- cycode/cyclient/headers.py | 3 +- cycode/cyclient/import_sbom_client.py | 7 +- cycode/cyclient/models.py | 66 ++-- cycode/cyclient/report_client.py | 3 +- cycode/cyclient/scan_client.py | 15 +- cycode/logger.py | 8 +- poetry.lock | 373 ++++++------------ process_executable_file.py | 5 +- pyinstaller.spec | 4 +- pyproject.toml | 30 +- tests/cli/apps/mcp/test_mcp_command.py | 4 - .../commands/ai_guardrails/ides/test_codex.py | 3 +- .../ai_guardrails/ides/test_copilot.py | 3 +- .../commands/ai_guardrails/scan/conftest.py | 7 +- .../ai_guardrails/scan/test_handlers.py | 8 +- .../scan/test_mcp_server_status.py | 5 +- .../ai_guardrails/scan/test_policy.py | 3 +- .../ai_guardrails/scan/test_scan_command.py | 7 +- .../test_session_start_command.py | 9 +- .../commands/version/test_version_checker.py | 4 +- .../sca/go/test_restore_go_dependencies.py | 5 +- .../maven/test_restore_gradle_dependencies.py | 9 +- .../maven/test_restore_maven_dependencies.py | 9 +- .../sca/npm/test_restore_bun_dependencies.py | 5 +- .../sca/npm/test_restore_npm_dependencies.py | 7 +- .../sca/npm/test_restore_pnpm_dependencies.py | 5 +- .../sca/npm/test_restore_yarn_dependencies.py | 5 +- .../files_collector/sca/npm/test_workspace.py | 3 +- .../nuget/test_restore_nuget_dependencies.py | 5 +- .../php/test_restore_composer_dependencies.py | 5 +- .../python/test_restore_pip_dependencies.py | 13 +- .../test_restore_pipenv_dependencies.py | 5 +- .../test_restore_poetry_dependencies.py | 5 +- .../python/test_restore_uv_dependencies.py | 5 +- .../ruby/test_restore_ruby_dependencies.py | 5 +- .../sca/sbt/test_restore_sbt_dependencies.py | 5 +- .../sca/test_base_restore_dependencies.py | 3 +- tests/cli/test_app_argv_peek.py | 3 +- tests/cli/utils/test_host_info.py | 5 +- tests/conftest.py | 7 +- .../mocked_responses/import_sbom_client.py | 6 +- .../cyclient/mocked_responses/scan_client.py | 13 +- tests/test_performance_get_all_files.py | 5 +- .../test_configuration_manager.py | 8 +- tests/utils/test_trust_store.py | 41 +- 164 files changed, 787 insertions(+), 1103 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index b21a7df6..919728b7 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -33,7 +33,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: '3.9' + python-version: '3.10' - name: Load cached Poetry setup id: cached_poetry @@ -61,10 +61,10 @@ jobs: echo "CLI_VERSION=$(poetry version --short)" >> $GITHUB_OUTPUT - name: Set up QEMU - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Login to Docker Hub if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') }} @@ -76,7 +76,7 @@ jobs: - name: Build and push id: docker_build if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') }} - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . platforms: linux/amd64,linux/arm64 @@ -86,7 +86,7 @@ jobs: - name: Verify build id: docker_verify_build if: ${{ github.event_name != 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/v') }} - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/pre_release.yml b/.github/workflows/pre_release.yml index ef35b05f..a84d01ed 100644 --- a/.github/workflows/pre_release.yml +++ b/.github/workflows/pre_release.yml @@ -35,7 +35,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: '3.9' + python-version: '3.10' - name: Load cached Poetry setup id: cached-poetry diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62d861ae..0fa4682f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: '3.9' + python-version: '3.10' - name: Load cached Poetry setup id: cached-poetry diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 098739d7..a6a218f6 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -26,7 +26,7 @@ jobs: - name: Setup Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: 3.9 + python-version: '3.10' - name: Load cached Poetry setup id: cached-poetry diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 33cfd6ab..72e38768 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,7 +28,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: '3.9' + python-version: '3.10' - name: Load cached Poetry setup id: cached-poetry diff --git a/.github/workflows/tests_full.yml b/.github/workflows/tests_full.yml index 9f99a66d..a92cb1a4 100644 --- a/.github/workflows/tests_full.yml +++ b/.github/workflows/tests_full.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: [ macos-latest, ubuntu-latest, windows-latest ] - python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ] + python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] runs-on: ${{matrix.os}} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 857a27cd..90f260a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ ## How to contribute to Cycode CLI -The minimum version of Python that we support is 3.9. +The minimum version of Python that we support is 3.10. We recommend using this version for local development. But it’s fine to use a higher version without using new features from these versions. diff --git a/README.md b/README.md index 4840651e..04e23bfd 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ This guide walks you through both installation and usage. # Prerequisites -- The Cycode CLI application requires Python version 3.9 or later. The MCP command is available only for Python 3.10 and above. If you're using an earlier Python version, this command will not be available. +- The Cycode CLI application requires Python version 3.10 or later. - Use the [`cycode auth` command](#using-the-auth-command) to authenticate to Cycode with the CLI - Alternatively, you can get a Cycode Client ID and Client Secret Key by following the steps detailed in the [Service Account Token](https://docs.cycode.com/docs/en/service-accounts) and [Personal Access Token](https://docs.cycode.com/v1/docs/managing-personal-access-tokens) pages, which contain details on getting these values. @@ -246,7 +246,7 @@ Perform the following steps to install the pre-commit hook: ### Installing Pre-Commit Hook -1. Install the pre-commit framework (Python 3.9 or higher must be installed): +1. Install the pre-commit framework (Python 3.10 or higher must be installed): ```bash pip3 install pre-commit @@ -395,9 +395,6 @@ The two options combine: with `CYCODE_CLI_ENABLE_TRUSTSTORE=1`, certificates fro Notes: -- `CYCODE_CLI_ENABLE_TRUSTSTORE` requires Python 3.10 or newer. On Python 3.9 the CLI logs a warning - and falls back to the bundled CA bundle; use `REQUESTS_CA_BUNDLE` instead, or upgrade Python. The - standalone executables and the Docker image already ship a supported Python. - On Windows, the CLI has always fallen back to the system certificate store when neither `REQUESTS_CA_BUNDLE` nor `CURL_CA_BUNDLE` is set. That behavior is unchanged. - Proxies themselves are configured with the standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` @@ -405,9 +402,6 @@ Notes: # MCP Command \[EXPERIMENT\] -> [!WARNING] -> The MCP command is available only for Python 3.10 and above. If you're using an earlier Python version, this command will not be available. - The Model Context Protocol (MCP) command allows you to start an MCP server that exposes Cycode's scanning capabilities to AI systems and applications. This enables AI models to interact with Cycode CLI tools via a standardized protocol. > [!TIP] diff --git a/cycode/cli/app.py b/cycode/cli/app.py index 82f7f41b..563fc110 100644 --- a/cycode/cli/app.py +++ b/cycode/cli/app.py @@ -1,7 +1,7 @@ import importlib import logging import sys -from typing import Annotated, Optional +from typing import Annotated import click import typer @@ -51,13 +51,12 @@ 'auth': 'cycode.cli.apps.auth', 'configure': 'cycode.cli.apps.configure', 'ignore': 'cycode.cli.apps.ignore', + 'mcp': 'cycode.cli.apps.mcp', 'report': 'cycode.cli.apps.report', 'import': 'cycode.cli.apps.report_import', 'scan': 'cycode.cli.apps.scan', 'status': 'cycode.cli.apps.status', } -if sys.version_info >= (3, 10): - _SUBAPP_MODULES['mcp'] = 'cycode.cli.apps.mcp' # Aliases: alternate spellings that resolve to a primary subcommand key. _SUBAPP_ALIASES: dict[str, str] = { @@ -80,7 +79,7 @@ ) -def _detect_invocation() -> tuple[Optional[str], Optional[str]]: +def _detect_invocation() -> tuple[str | None, str | None]: """Return (top-level-subapp, second-level-subcommand) parsed from sys.argv. Both values may be None: when no positional arg matches a known subapp, @@ -113,7 +112,7 @@ def _detect_invocation() -> tuple[Optional[str], Optional[str]]: _INVOKED_SUBAPP, _INVOKED_SUBCOMMAND = _detect_invocation() -def _register_subapps(only: Optional[str]) -> None: +def _register_subapps(only: str | None) -> None: if only is not None: app.add_typer(importlib.import_module(_SUBAPP_MODULES[only]).app) return @@ -189,32 +188,32 @@ def app_callback( OutputTypeOption, typer.Option('--output', '-o', case_sensitive=False, help='Specify the output type.') ] = OutputTypeOption.RICH, user_agent: Annotated[ - Optional[str], + str | None, typer.Option(hidden=True, help='Characteristic JSON object that lets servers identify the application.'), ] = None, client_secret: Annotated[ - Optional[str], + str | None, typer.Option( help='Specify a Cycode client secret for this specific scan execution.', rich_help_panel=_AUTH_RICH_HELP_PANEL, ), ] = None, client_id: Annotated[ - Optional[str], + str | None, typer.Option( help='Specify a Cycode client ID for this specific scan execution.', rich_help_panel=_AUTH_RICH_HELP_PANEL, ), ] = None, id_token: Annotated[ - Optional[str], + str | None, typer.Option( help='Specify a Cycode OIDC ID token for this specific scan execution.', rich_help_panel=_AUTH_RICH_HELP_PANEL, ), ] = None, _: Annotated[ - Optional[bool], + bool | None, typer.Option( '--install-completion', callback=install_callback, diff --git a/cycode/cli/apps/activation_manager.py b/cycode/cli/apps/activation_manager.py index 8eed3caa..ccc1d4b1 100644 --- a/cycode/cli/apps/activation_manager.py +++ b/cycode/cli/apps/activation_manager.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from cycode import __version__ from cycode.cli.config import configuration_manager @@ -13,13 +13,13 @@ _CLI_CLIENT_NAME = 'cli' -def _get_client_and_version(plugin_app_name: Optional[str], plugin_app_version: Optional[str]) -> tuple[str, str]: +def _get_client_and_version(plugin_app_name: str | None, plugin_app_version: str | None) -> tuple[str, str]: return plugin_app_name or _CLI_CLIENT_NAME, plugin_app_version or __version__ def should_report_cli_activation( - plugin_app_name: Optional[str] = None, - plugin_app_version: Optional[str] = None, + plugin_app_name: str | None = None, + plugin_app_version: str | None = None, ) -> bool: client, version = _get_client_and_version(plugin_app_name, plugin_app_version) return configuration_manager.get_last_reported_activation_version(client) != version @@ -27,8 +27,8 @@ def should_report_cli_activation( def report_cli_activation( cycode_client: 'CycodeClientBase', - plugin_app_name: Optional[str] = None, - plugin_app_version: Optional[str] = None, + plugin_app_name: str | None = None, + plugin_app_version: str | None = None, ) -> None: """Report CLI/IDE activation to the backend if the (client, version) pair is new. diff --git a/cycode/cli/apps/ai_guardrails/command_utils.py b/cycode/cli/apps/ai_guardrails/command_utils.py index 291fabcf..6fa61438 100644 --- a/cycode/cli/apps/ai_guardrails/command_utils.py +++ b/cycode/cli/apps/ai_guardrails/command_utils.py @@ -2,7 +2,6 @@ import os from pathlib import Path -from typing import Optional import typer from rich.console import Console @@ -18,7 +17,7 @@ def validate_scope(scope: str, allowed_scopes: tuple[str, ...] = ('user', 'repo' raise typer.Exit(1) -def resolve_repo_path(scope: str, repo_path: Optional[Path]) -> Optional[Path]: +def resolve_repo_path(scope: str, repo_path: Path | None) -> Path | None: """Default repo_path to cwd for 'repo' scope; leave None for 'user' scope.""" if scope == 'repo' and repo_path is None: return Path(os.getcwd()) diff --git a/cycode/cli/apps/ai_guardrails/hooks_manager.py b/cycode/cli/apps/ai_guardrails/hooks_manager.py index d45818e7..eacdaa19 100644 --- a/cycode/cli/apps/ai_guardrails/hooks_manager.py +++ b/cycode/cli/apps/ai_guardrails/hooks_manager.py @@ -8,7 +8,6 @@ import copy import json from pathlib import Path -from typing import Optional import yaml @@ -47,7 +46,7 @@ def is_cycode_hook_entry(entry: dict) -> bool: return False -def _strip_cycode_from_entry(entry: dict) -> Optional[dict]: +def _strip_cycode_from_entry(entry: dict) -> dict | None: """Remove Cycode hooks from ``entry`` and return the remainder. Returns ``None`` when nothing useful remains (Cursor-flat Cycode entry, or @@ -72,7 +71,7 @@ def _strip_cycode_from_entry(entry: dict) -> Optional[dict]: return entry -def _load_hooks_file(hooks_path: Path) -> Optional[dict]: +def _load_hooks_file(hooks_path: Path) -> dict | None: if not hooks_path.exists(): return None try: @@ -102,7 +101,7 @@ def _load_policy_dict(policy_path: Path) -> dict: return {**copy.deepcopy(DEFAULT_POLICY), **existing} -def create_policy_file(scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]: +def create_policy_file(scope: str, repo_path: Path | None = None) -> tuple[bool, str]: """Create or update the ai-guardrails.yaml policy file (operational knobs only). Enforcement mode and sensitive-path globs are platform-managed; those keys are stripped @@ -125,7 +124,7 @@ def create_policy_file(scope: str, repo_path: Optional[Path] = None) -> tuple[bo def install_hooks( ide: IDE, scope: str = 'user', - repo_path: Optional[Path] = None, + repo_path: Path | None = None, ) -> tuple[bool, str]: """Install Cycode AI guardrails hooks for ``ide``.""" hooks_path = ide.settings_path(scope, repo_path) @@ -197,7 +196,7 @@ def _persist_uninstall(hooks_path: Path, existing: dict, modified: bool) -> tupl return True, f'Cycode hooks removed from: {hooks_path}' -def uninstall_hooks(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = None) -> tuple[bool, str]: +def uninstall_hooks(ide: IDE, scope: str = 'user', repo_path: Path | None = None) -> tuple[bool, str]: """Remove Cycode AI guardrails hooks for ``ide``.""" hooks_path = ide.settings_path(scope, repo_path) @@ -218,7 +217,7 @@ def uninstall_hooks(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = N return True, message -def get_hooks_status(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = None) -> dict: +def get_hooks_status(ide: IDE, scope: str = 'user', repo_path: Path | None = None) -> dict: """Return installation status of Cycode hooks for ``ide``.""" hooks_path = ide.settings_path(scope, repo_path) diff --git a/cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py b/cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py index d10e7e56..39fce7e2 100644 --- a/cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +++ b/cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py @@ -7,15 +7,16 @@ """ import json +from collections.abc import Callable from pathlib import Path -from typing import Any, Callable, Optional +from typing import Any from cycode.logger import get_logger logger = get_logger('AI Guardrails Plugins') -def resolve_cached_plugin_dir(cache_root: Path, marketplace: str, plugin_name: str) -> Optional[Path]: +def resolve_cached_plugin_dir(cache_root: Path, marketplace: str, plugin_name: str) -> Path | None: """Find ``////``. Both Claude Code and Codex cache installed plugin content in this layout (the trailing @@ -31,7 +32,7 @@ def resolve_cached_plugin_dir(cache_root: Path, marketplace: str, plugin_name: s return max(candidates, key=lambda d: (d.stat().st_mtime, d.name)) -def load_plugin_json(path: Path) -> Optional[dict]: +def load_plugin_json(path: Path) -> dict | None: """Load a JSON file inside a plugin directory; None if missing or invalid.""" if not path.exists(): return None @@ -42,7 +43,7 @@ def load_plugin_json(path: Path) -> Optional[dict]: return None -def build_global_config_files(path: Path, mcp_servers: Optional[dict]) -> list[dict]: +def build_global_config_files(path: Path, mcp_servers: dict | None) -> list[dict]: """Wrap a global (non-plugin) MCP config into the session-context file shape. Returns ``[{"path": , "content": <{"mcpServers": ...} JSON>}]`` when @@ -58,7 +59,7 @@ def build_global_config_files(path: Path, mcp_servers: Optional[dict]) -> list[d def walk_enabled_plugins( plugin_entries: dict[str, Any], is_enabled: Callable[[Any], bool], - locate_dir: Callable[[str, str], Optional[Path]], + locate_dir: Callable[[str, str], Path | None], read_plugin: Callable[[Path], tuple[dict, dict]], ) -> dict: """Iterate enabled plugins and build their inventory metadata. diff --git a/cycode/cli/apps/ai_guardrails/ides/_skill_utils.py b/cycode/cli/apps/ai_guardrails/ides/_skill_utils.py index 711681d4..d8b57639 100644 --- a/cycode/cli/apps/ai_guardrails/ides/_skill_utils.py +++ b/cycode/cli/apps/ai_guardrails/ides/_skill_utils.py @@ -11,7 +11,6 @@ """ from pathlib import Path -from typing import Optional from cycode.logger import get_logger @@ -32,7 +31,7 @@ MAX_SKILLS_PER_ROOT = 200 -def _read_skill_file(skill_file: Path) -> Optional[dict]: +def _read_skill_file(skill_file: Path) -> dict | None: """Read one ``SKILL.md`` into the session-context file shape, or None if unusable.""" try: size = skill_file.stat().st_size diff --git a/cycode/cli/apps/ai_guardrails/ides/base.py b/cycode/cli/apps/ai_guardrails/ides/base.py index 4bf0c1ac..ab5958b4 100644 --- a/cycode/cli/apps/ai_guardrails/ides/base.py +++ b/cycode/cli/apps/ai_guardrails/ides/base.py @@ -18,7 +18,7 @@ from dataclasses import dataclass from enum import Enum from pathlib import Path -from typing import ClassVar, Optional +from typing import ClassVar from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType @@ -44,17 +44,15 @@ class HookDecision: action: DecisionAction event_type: AiHookEventType - user_message: Optional[str] = None - agent_message: Optional[str] = None + user_message: str | None = None + agent_message: str | None = None @classmethod def allow(cls, event_type: AiHookEventType) -> 'HookDecision': return cls(action=DecisionAction.ALLOW, event_type=event_type) @classmethod - def deny( - cls, event_type: AiHookEventType, user_message: str, agent_message: Optional[str] = None - ) -> 'HookDecision': + def deny(cls, event_type: AiHookEventType, user_message: str, agent_message: str | None = None) -> 'HookDecision': return cls( action=DecisionAction.DENY, event_type=event_type, @@ -63,7 +61,7 @@ def deny( ) @classmethod - def ask(cls, event_type: AiHookEventType, user_message: str, agent_message: Optional[str] = None) -> 'HookDecision': + def ask(cls, event_type: AiHookEventType, user_message: str, agent_message: str | None = None) -> 'HookDecision': return cls( action=DecisionAction.ASK, event_type=event_type, @@ -92,7 +90,7 @@ class IDE(ABC): # --- install / status --- @abstractmethod - def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path: + def settings_path(self, scope: str, repo_path: Path | None = None) -> Path: """Return the hooks/settings file path for the given scope. `scope` is 'user' or 'repo'. `repo_path` is required when scope == 'repo'. @@ -108,7 +106,7 @@ def render_hooks_config(self) -> dict: ``hooks_manager`` can treat them uniformly. """ - def post_install(self, scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]: + def post_install(self, scope: str, repo_path: Path | None = None) -> tuple[bool, str]: """Run IDE-specific actions after the hooks file is written. Default: no-op success. Override to perform extra setup that doesn't @@ -120,7 +118,7 @@ def post_install(self, scope: str, repo_path: Optional[Path] = None) -> tuple[bo """ return True, '' - def post_uninstall(self, scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]: + def post_uninstall(self, scope: str, repo_path: Path | None = None) -> tuple[bool, str]: """Run IDE-specific cleanup after the hooks file is removed. Default: no-op success. Override to undo whatever ``post_install`` @@ -169,7 +167,7 @@ def build_session_payload(self, raw_payload: dict) -> AIHookPayload: """ return AIHookPayload(ide_provider=self.name) - def get_user_email(self) -> Optional[str]: + def get_user_email(self) -> str | None: """Best-effort read of the user's email from IDE-specific config. Default: None. Override if the IDE stores a usable account locally. diff --git a/cycode/cli/apps/ai_guardrails/ides/claude_code.py b/cycode/cli/apps/ai_guardrails/ides/claude_code.py index b95dd593..c70c363f 100644 --- a/cycode/cli/apps/ai_guardrails/ides/claude_code.py +++ b/cycode/cli/apps/ai_guardrails/ides/claude_code.py @@ -5,7 +5,7 @@ from collections.abc import Iterator from copy import deepcopy from pathlib import Path -from typing import ClassVar, Optional +from typing import ClassVar from cycode.cli.apps.ai_guardrails.consts import CYCODE_SCAN_PROMPT_COMMAND, CYCODE_SESSION_START_COMMAND from cycode.cli.apps.ai_guardrails.ides._plugin_utils import ( @@ -80,12 +80,12 @@ def _reverse_readline(path: Path, buf_size: int = 8192) -> Iterator[str]: yield buffer.decode('utf-8', errors='replace') -def _extract_model(entry: dict) -> Optional[str]: +def _extract_model(entry: dict) -> str | None: """Extract model from a transcript entry (top level or nested in message).""" return entry.get('model') or (entry.get('message') or {}).get('model') -def _extract_generation_id(entry: dict) -> Optional[str]: +def _extract_generation_id(entry: dict) -> str | None: """Extract generation ID from a user-type transcript entry.""" if entry.get('type') == 'user': return entry.get('uuid') @@ -94,7 +94,7 @@ def _extract_generation_id(entry: dict) -> Optional[str]: def extract_from_claude_transcript( transcript_path: str, -) -> tuple[Optional[str], Optional[str], Optional[str]]: +) -> tuple[str | None, str | None, str | None]: """Extract ``(ide_version, model, generation_id)`` from a transcript. The transcript is a JSONL file scanned from end → start so the most recent @@ -135,7 +135,7 @@ def extract_from_claude_transcript( # --- ~/.claude.json + ~/.claude/settings.json parsing ------------------------- -def load_claude_config(config_path: Optional[Path] = None) -> Optional[dict]: +def load_claude_config(config_path: Path | None = None) -> dict | None: """Load and parse `~/.claude.json`. Returns None if missing/invalid.""" path = config_path or _CLAUDE_CONFIG_PATH if not path.exists(): @@ -148,17 +148,17 @@ def load_claude_config(config_path: Optional[Path] = None) -> Optional[dict]: return None -def _email_from_config(config: dict) -> Optional[str]: +def _email_from_config(config: dict) -> str | None: """Read ``oauthAccount.emailAddress`` from a parsed Claude config.""" return config.get('oauthAccount', {}).get('emailAddress') -def get_mcp_servers(config: dict) -> Optional[dict]: +def get_mcp_servers(config: dict) -> dict | None: """Read ``mcpServers`` from a parsed Claude config.""" return config.get('mcpServers') -def load_claude_settings(settings_path: Optional[Path] = None) -> Optional[dict]: +def load_claude_settings(settings_path: Path | None = None) -> dict | None: """Load and parse `~/.claude/settings.json`. Returns None if missing/invalid.""" path = settings_path or _CLAUDE_SETTINGS_PATH if not path.exists(): @@ -185,7 +185,7 @@ def _plugins_cache_dir() -> Path: return Path.home() / '.claude' / 'plugins' / 'cache' -def _resolve_marketplace_path(marketplace: dict) -> Optional[Path]: +def _resolve_marketplace_path(marketplace: dict) -> Path | None: """Resolve filesystem path for a directory-type marketplace.""" source = marketplace.get('source', {}) if source.get('source') != 'directory': @@ -236,7 +236,7 @@ def resolve_plugins(settings: dict) -> dict: enabled = settings.get('enabledPlugins') or {} marketplaces = settings.get('extraKnownMarketplaces') or {} - def _locate(plugin_name: str, marketplace_name: str) -> Optional[Path]: + def _locate(plugin_name: str, marketplace_name: str) -> 'Path | None': # Directory-type marketplaces point straight at the plugin source; every other source # type (git, github, ...) is cloned into the local plugin cache. marketplace = marketplaces.get(marketplace_name) @@ -259,7 +259,7 @@ def _sanitize_mcp_name(name: str) -> str: return re.sub(r'[^a-z0-9_-]', '_', name.lower()) -def _local_mcp_servers(config: Optional[dict], cwd: Optional[str]) -> Iterator[tuple[str, str]]: +def _local_mcp_servers(config: dict | None, cwd: str | None) -> Iterator[tuple[str, str]]: """Yield ``(name as Claude Code namespaces it, config key)`` for each locally configured MCP server.""" sources = [] if config: @@ -280,7 +280,7 @@ def _local_mcp_servers(config: Optional[dict], cwd: Optional[str]) -> Iterator[t yield f'plugin_{plugin_name}_{server}', server -def _resolve_mcp_server_name(server: str, config: Optional[dict], cwd: Optional[str]) -> str: +def _resolve_mcp_server_name(server: str, config: dict | None, cwd: str | None) -> str: """Map the server segment of ``mcp____`` back to its config key; unknown names pass through.""" wanted = _sanitize_mcp_name(server) for namespaced, name in _local_mcp_servers(config, cwd): @@ -297,7 +297,7 @@ class ClaudeCode(IDE): display_name: ClassVar[str] = 'Claude Code' hook_events: ClassVar[list[str]] = list(_HOOK_EVENTS) - def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path: + def settings_path(self, scope: str, repo_path: Path | None = None) -> Path: if scope == 'repo' and repo_path: return repo_path / _REPO_SUBDIR / _HOOKS_FILE_NAME return _USER_HOOKS_DIR / _HOOKS_FILE_NAME @@ -430,7 +430,7 @@ def build_session_payload(self, raw_payload: dict) -> AIHookPayload: source=raw_payload.get('source'), ) - def get_user_email(self) -> Optional[str]: + def get_user_email(self) -> str | None: config = load_claude_config() return _email_from_config(config) if config else None diff --git a/cycode/cli/apps/ai_guardrails/ides/codex.py b/cycode/cli/apps/ai_guardrails/ides/codex.py index b3918d57..a41e6f12 100644 --- a/cycode/cli/apps/ai_guardrails/ides/codex.py +++ b/cycode/cli/apps/ai_guardrails/ides/codex.py @@ -6,7 +6,7 @@ import sys from collections.abc import Iterator from pathlib import Path -from typing import ClassVar, Optional +from typing import ClassVar import tomli_w @@ -61,14 +61,14 @@ def _codex_skills_dir() -> Path: return _codex_home() / 'skills' -def _codex_config_toml_path(scope: str, repo_path: Optional[Path] = None) -> Path: +def _codex_config_toml_path(scope: str, repo_path: Path | None = None) -> Path: """Return the Codex ``config.toml`` path for the given scope.""" if scope == 'repo' and repo_path: return repo_path / _CONFIG_DIR_NAME / _CONFIG_TOML_NAME return _codex_home() / _CONFIG_TOML_NAME -def _load_codex_config(config_path: Optional[Path] = None) -> Optional[dict]: +def _load_codex_config(config_path: Path | None = None) -> dict | None: """Load and parse Codex's ``config.toml``. Returns None on missing/invalid.""" path = config_path or (_codex_home() / _CONFIG_TOML_NAME) if not path.exists(): @@ -82,7 +82,7 @@ def _load_codex_config(config_path: Optional[Path] = None) -> Optional[dict]: return None -def _email_from_auth(auth_path: Optional[Path] = None) -> Optional[str]: +def _email_from_auth(auth_path: Path | None = None) -> str | None: """Best-effort extraction of the signed-in Codex user's email. Reads ``~/.codex/auth.json`` and decodes the JWT in ``tokens.id_token`` @@ -108,7 +108,7 @@ def _email_from_auth(auth_path: Optional[Path] = None) -> Optional[str]: return claims.get('email') -def _resolve_codex_plugin_dir(plugin_name: str, marketplace: str) -> Optional[Path]: +def _resolve_codex_plugin_dir(plugin_name: str, marketplace: str) -> Path | None: """Find ``~/.codex/plugins/cache////``.""" return resolve_cached_plugin_dir(_codex_home() / 'plugins' / 'cache', marketplace, plugin_name) @@ -177,13 +177,13 @@ def _local_mcp_server_names() -> Iterator[str]: yield from entry.get('mcp_server_names') or [] -def _split_mcp_tool_name(tool_name: str) -> tuple[Optional[str], Optional[str]]: +def _split_mcp_tool_name(tool_name: str) -> tuple[str | None, str | None]: """Split ``mcp____`` into ``(server config key, tool)``; unknown namespaces split on ``__``.""" rest = tool_name[len('mcp__') :] names = list(_local_mcp_server_names()) # Codex appends `_<12 hex>` to a namespace on collision or length overflow. for suffix in ('__', r'_+[0-9a-f]{12}__'): - best: Optional[tuple[int, str]] = None + best: tuple[int, str] | None = None for name in names: namespace = _sanitize_mcp_name(name).rstrip('_') match = namespace and re.match(re.escape(namespace) + suffix, rest) @@ -196,7 +196,7 @@ def _split_mcp_tool_name(tool_name: str) -> tuple[Optional[str], Optional[str]]: return (parts[1] if len(parts) >= 2 else None), (parts[2] if len(parts) >= 3 else None) -def _enable_codex_hooks_feature(scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]: +def _enable_codex_hooks_feature(scope: str, repo_path: Path | None = None) -> tuple[bool, str]: """Set ``[features] hooks = true`` in Codex's ``config.toml``. Codex's hook scripts are gated behind this feature flag. We preserve any @@ -234,7 +234,7 @@ class Codex(IDE): display_name: ClassVar[str] = 'Codex' hook_events: ClassVar[list[str]] = list(_HOOK_EVENTS) - def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path: + def settings_path(self, scope: str, repo_path: Path | None = None) -> Path: if scope == 'repo' and repo_path: return repo_path / _CONFIG_DIR_NAME / _HOOKS_FILE_NAME return _codex_home() / _HOOKS_FILE_NAME @@ -264,7 +264,7 @@ def render_hooks_config(self) -> dict: }, } - def post_install(self, scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]: + def post_install(self, scope: str, repo_path: Path | None = None) -> tuple[bool, str]: return _enable_codex_hooks_feature(scope, repo_path) def matches_payload(self, raw_payload: dict) -> bool: @@ -333,7 +333,7 @@ def build_session_payload(self, raw_payload: dict) -> AIHookPayload: source=raw_payload.get('source'), ) - def get_user_email(self) -> Optional[str]: + def get_user_email(self) -> str | None: return _email_from_auth() def get_session_context(self) -> tuple[list[dict], dict]: diff --git a/cycode/cli/apps/ai_guardrails/ides/copilot.py b/cycode/cli/apps/ai_guardrails/ides/copilot.py index 77118ed5..90b71028 100644 --- a/cycode/cli/apps/ai_guardrails/ides/copilot.py +++ b/cycode/cli/apps/ai_guardrails/ides/copilot.py @@ -27,7 +27,7 @@ import re from collections.abc import Iterable from pathlib import Path -from typing import ClassVar, Optional, Union +from typing import ClassVar from urllib.parse import urlparse from urllib.request import url2pathname @@ -131,7 +131,7 @@ def _vscode_mcp_config_path() -> Path: return _vscode_user_dir() / _MCP_CONFIG_FILENAME -def _load_vscode_mcp_config(config_path: Optional[Path] = None) -> Optional[dict]: +def _load_vscode_mcp_config(config_path: Path | None = None) -> dict | None: """Load and parse VS Code's user-level ``mcp.json``. Returns None if missing/invalid.""" path = config_path or _vscode_mcp_config_path() if not path.exists(): @@ -144,7 +144,7 @@ def _load_vscode_mcp_config(config_path: Optional[Path] = None) -> Optional[dict return None -def _load_jsonc(path: Path) -> Optional[dict]: +def _load_jsonc(path: Path) -> dict | None: """Parse a JSON file tolerating //-comment lines (Copilot's config.json ships with a comment header; VS Code's settings.json is JSONC). @@ -321,7 +321,7 @@ def _server_name_variants(server_name: str) -> set[str]: return {v for v in (server_name, underscored, collapsed) if v} -def _read_file_path(tool_name: str, tool_input: object) -> Optional[str]: +def _read_file_path(tool_name: str, tool_input: object) -> str | None: """Path of a file-read tool call, or None when this isn't one. The agent runtime reuses its read tool for directory listings, with a payload @@ -350,7 +350,7 @@ def is_mcp_tool_name(tool_name: str) -> bool: return tool_name.startswith(_MCP_TOOL_PREFIX) or _MCP_AGENT_SEPARATOR in tool_name -def split_mcp_tool_name(tool_name: str, server_names: Iterable[str]) -> tuple[Optional[str], Optional[str]]: +def split_mcp_tool_name(tool_name: str, server_names: Iterable[str]) -> tuple[str | None, str | None]: """Split an MCP tool name into ``(server, tool)``. Handles both naming schemes: VS Code's ``mcp__`` and the agent @@ -385,7 +385,7 @@ class Copilot(IDE): display_name: ClassVar[str] = 'GitHub Copilot' hook_events: ClassVar[list[str]] = list(_HOOK_EVENTS) - def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path: + def settings_path(self, scope: str, repo_path: Path | None = None) -> Path: # Dedicated Cycode-owned file (Copilot reads every *.json in the hooks # dir), unlike the shared settings files of other IDEs. if scope == 'repo' and repo_path: @@ -419,7 +419,7 @@ def parse_hook_payload(self, raw_payload: dict) -> AIHookPayload: read_path = _read_file_path(tool_name, tool_input) if hook_event_name == 'UserPromptSubmit': - canonical_event: Union[AiHookEventType, str] = AiHookEventType.PROMPT + canonical_event: AiHookEventType | str = AiHookEventType.PROMPT elif hook_event_name == 'PreToolUse' and read_path is not None: canonical_event = AiHookEventType.FILE_READ elif hook_event_name == 'PreToolUse' and is_mcp_tool_name(tool_name): diff --git a/cycode/cli/apps/ai_guardrails/ides/cursor.py b/cycode/cli/apps/ai_guardrails/ides/cursor.py index 14ac0d0b..4fbf070e 100644 --- a/cycode/cli/apps/ai_guardrails/ides/cursor.py +++ b/cycode/cli/apps/ai_guardrails/ides/cursor.py @@ -3,7 +3,7 @@ import json import platform from pathlib import Path -from typing import ClassVar, Optional +from typing import ClassVar from cycode.cli.apps.ai_guardrails.consts import CYCODE_SCAN_PROMPT_COMMAND, CYCODE_SESSION_START_COMMAND from cycode.cli.apps.ai_guardrails.ides._plugin_utils import build_global_config_files @@ -51,7 +51,7 @@ def _cursor_skills_dir() -> Path: return Path.home() / '.cursor' / 'skills' -def _load_cursor_mcp_config(config_path: Optional[Path] = None) -> Optional[dict]: +def _load_cursor_mcp_config(config_path: Path | None = None) -> dict | None: """Load and parse `~/.cursor/mcp.json`. Returns None if missing/invalid.""" path = config_path or _cursor_mcp_config_path() if not path.exists(): @@ -71,7 +71,7 @@ def _cursor_mcp_config_paths(workspace_roots: object) -> list[Path]: return paths -def _server_command_line(server: dict) -> Optional[str]: +def _server_command_line(server: dict) -> str | None: command = server.get('command') if not isinstance(command, str) or not command: return None @@ -79,7 +79,7 @@ def _server_command_line(server: dict) -> Optional[str]: return ' '.join([command, *(str(arg) for arg in args)]) if isinstance(args, list) else command -def _resolve_mcp_server_name(raw_payload: dict) -> Optional[str]: +def _resolve_mcp_server_name(raw_payload: dict) -> str | None: """Cursor sends the server's url or command, but the platform knows it by its mcp.json entry name.""" url = raw_payload.get('url') command = raw_payload.get('command') @@ -107,7 +107,7 @@ class Cursor(IDE): display_name: ClassVar[str] = 'Cursor' hook_events: ClassVar[list[str]] = list(_CURSOR_EVENT_MAPPING) - def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path: + def settings_path(self, scope: str, repo_path: Path | None = None) -> Path: if scope == 'repo' and repo_path: return repo_path / _REPO_SUBDIR / _HOOKS_FILE_NAME return _user_hooks_dir() / _HOOKS_FILE_NAME diff --git a/cycode/cli/apps/ai_guardrails/install_command.py b/cycode/cli/apps/ai_guardrails/install_command.py index c1407905..f9eef20d 100644 --- a/cycode/cli/apps/ai_guardrails/install_command.py +++ b/cycode/cli/apps/ai_guardrails/install_command.py @@ -1,7 +1,7 @@ """Install command for AI guardrails hooks.""" from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer @@ -29,7 +29,7 @@ def install_command( ), ] = DEFAULT_IDE_NAME, repo_path: Annotated[ - Optional[Path], + Path | None, typer.Option( '--repo-path', help='Repository path for repo-scoped installation (defaults to current directory).', @@ -93,7 +93,7 @@ def install_command( raise typer.Exit(1) -def _install_policy(scope: str, repo_path: Optional[Path]) -> None: +def _install_policy(scope: str, repo_path: Path | None) -> None: policy_success, policy_message = create_policy_file(scope, repo_path) if policy_success: console.print(f'[green]✓[/] {policy_message}') diff --git a/cycode/cli/apps/ai_guardrails/scan/guardrail_config.py b/cycode/cli/apps/ai_guardrails/scan/guardrail_config.py index a2e95d61..d1e59f98 100644 --- a/cycode/cli/apps/ai_guardrails/scan/guardrail_config.py +++ b/cycode/cli/apps/ai_guardrails/scan/guardrail_config.py @@ -10,7 +10,6 @@ import time from dataclasses import dataclass, field from pathlib import Path -from typing import Optional from cycode.cli.apps.ai_guardrails.consts import GuardrailCellMode, PolicyMode from cycode.cli.apps.ai_guardrails.scan.consts import DEFAULT_SENSITIVE_PATH_GLOBS @@ -60,7 +59,7 @@ def _default_sensitive_globs() -> list: class GuardrailConfig: payload: dict fetched_at: float - tenant_id: Optional[str] = None + tenant_id: str | None = None _guardrails: dict = field(init=False, repr=False) def __post_init__(self) -> None: @@ -77,7 +76,7 @@ def ttl_seconds(self) -> float: def _agents(self, guardrail_key: str) -> dict: return (self._guardrails.get(guardrail_key) or {}).get('agents') or {} - def mode_for(self, guardrail_key: str, ide_name: Optional[str]) -> str: + def mode_for(self, guardrail_key: str, ide_name: str | None) -> str: """The platform keys the cells by our --ide names, so the lookup is direct.""" agents = self._agents(guardrail_key) return str(agents.get((ide_name or '').lower(), default_mode_for(guardrail_key))).lower() @@ -85,19 +84,19 @@ def mode_for(self, guardrail_key: str, ide_name: Optional[str]) -> str: def is_off_for_every_agent(self, guardrail_key: str) -> bool: return all(str(mode).lower() == GuardrailCellMode.OFF for mode in self._agents(guardrail_key).values()) - def _modes_for_event(self, event_name: str, ide_name: Optional[str]) -> list: + def _modes_for_event(self, event_name: str, ide_name: str | None) -> list: return [ self.mode_for(key, ide_name) for key, guardrail in self._guardrails.items() if str(guardrail.get('event_type', '')).lower() == str(event_name).lower() ] - def is_event_off(self, event_name: str, ide_name: Optional[str]) -> bool: + def is_event_off(self, event_name: str, ide_name: str | None) -> bool: """Every guardrail for this event is Off - skip the scan entirely.""" modes = self._modes_for_event(event_name, ide_name) return bool(modes) and all(mode == GuardrailCellMode.OFF for mode in modes) - def can_event_block(self, event_name: str, ide_name: Optional[str]) -> bool: + def can_event_block(self, event_name: str, ide_name: str | None) -> bool: """At least one guardrail for this event is in Block mode - the scan must stay synchronous.""" return GuardrailCellMode.BLOCK in self._modes_for_event(event_name, ide_name) @@ -109,12 +108,12 @@ def sensitive_globs(self) -> list: def is_expired(self) -> bool: return time.time() - self.fetched_at > self.ttl_seconds - def needs_refresh(self, tenant_id: Optional[str]) -> bool: + def needs_refresh(self, tenant_id: str | None) -> bool: """Expired, or fetched for another tenant (the user switched tenants since).""" return self.is_expired() or self.tenant_id != tenant_id -def apply_platform_config(policy: dict, config: Optional[GuardrailConfig], ide_name: Optional[str]) -> None: +def apply_platform_config(policy: dict, config: GuardrailConfig | None, ide_name: str | None) -> None: """Overlay the platform-owned enforcement config onto the local knobs-only policy. The platform is the only mode source: no cache (cold start) means the built-in defaults - @@ -149,7 +148,7 @@ def action(guardrail_key: str) -> str: mcp['server_action'] = action(BlockReason.UNAUTHORIZED_MCP_SERVER) -def save_guardrail_config(payload: dict, tenant_id: Optional[str]) -> None: +def save_guardrail_config(payload: dict, tenant_id: str | None) -> None: """Persist a fetched resolved config; a failed write just leaves the previous cache in place.""" path = get_config_cache_path() content = {'fetched_at': time.time(), 'tenant_id': tenant_id, 'payload': payload} @@ -160,7 +159,7 @@ def save_guardrail_config(payload: dict, tenant_id: Optional[str]) -> None: logger.debug('Failed to save guardrail config cache', exc_info=e) -def load_guardrail_config() -> Optional[GuardrailConfig]: +def load_guardrail_config() -> GuardrailConfig | None: """The cached platform config, or None when it is absent or corrupt (quarantined).""" path = get_config_cache_path() if not path.exists(): @@ -171,7 +170,7 @@ def load_guardrail_config() -> Optional[GuardrailConfig]: content = json.load(file) payload = content['payload'] if not isinstance(payload, dict): - raise ValueError('payload is not an object') + raise TypeError('payload is not an object') return GuardrailConfig( payload=payload, fetched_at=float(content['fetched_at']), tenant_id=content.get('tenant_id') ) diff --git a/cycode/cli/apps/ai_guardrails/scan/handlers.py b/cycode/cli/apps/ai_guardrails/scan/handlers.py index e977291a..27b40dd2 100644 --- a/cycode/cli/apps/ai_guardrails/scan/handlers.py +++ b/cycode/cli/apps/ai_guardrails/scan/handlers.py @@ -11,8 +11,9 @@ import json import os import threading +from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Callable, NamedTuple, Optional +from typing import TYPE_CHECKING, Any, NamedTuple import typer @@ -48,15 +49,15 @@ class ScanOutcome(NamedTuple): """What one guardrail scan came back with; the verdict is the server's, which applied the tenant's floors.""" - violation_summary: Optional[str] = None - scan_id: Optional[str] = None - verdict: Optional[GuardrailsMode] = None + violation_summary: str | None = None + scan_id: str | None = None + verdict: GuardrailsMode | None = None NO_SCAN = ScanOutcome() -def _parse_verdict(verdict: Optional[str]) -> Optional[GuardrailsMode]: +def _parse_verdict(verdict: str | None) -> GuardrailsMode | None: """The server spells the verdict "Block"/"Report" and omits it when the scan found nothing to decide on.""" if not verdict: return None @@ -105,7 +106,7 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli ) block_reason = BlockReason.SCAN_FAILURE error_message = str(e) - raise e + raise finally: ai_client.create_event( payload, @@ -192,7 +193,7 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy: ) block_reason = BlockReason.SCAN_FAILURE error_message = str(e) - raise e + raise finally: ai_client.create_event( payload, @@ -236,7 +237,7 @@ def _handle_arg_scan( policy: dict, feature: _ArgScanFeature, scan_text: str, - pre_scan_finding: Optional[_PreScanFinding] = None, + pre_scan_finding: _PreScanFinding | None = None, ) -> HookDecision: """Shared scan + decision flow for MCP_EXECUTION and COMMAND_EXEC events. @@ -297,7 +298,7 @@ def _handle_arg_scan( ) block_reason = BlockReason.SCAN_FAILURE error_message = str(e) - raise e + raise finally: ai_client.create_event( payload, @@ -309,7 +310,7 @@ def _handle_arg_scan( ) -def _check_mcp_server_authorization(payload: AIHookPayload, policy: dict) -> Optional[_PreScanFinding]: +def _check_mcp_server_authorization(payload: AIHookPayload, policy: dict) -> _PreScanFinding | None: """Fails open: no server name or no cached statuses lets the call through.""" mcp_config = get_policy_value(policy, 'mcp', default={}) if not get_policy_value(mcp_config, 'check_server', default=False): @@ -366,7 +367,7 @@ def handle_before_mcp_execution(ctx: typer.Context, payload: AIHookPayload, poli ) -def get_handler_for_event(event_type: str) -> Optional[HandlerFn]: +def get_handler_for_event(event_type: str) -> HandlerFn | None: """Look up the handler for a canonical event type.""" handlers: dict[str, HandlerFn] = { AiHookEventType.PROMPT.value: handle_before_submit_prompt, @@ -383,10 +384,10 @@ def get_effective_mode(feature_config: dict, action_key: str = 'action') -> Guar def should_detach_scan( - config: Optional['GuardrailConfig'], + config: 'GuardrailConfig | None', policy: dict, event_name: str, - ide_name: Optional[str], + ide_name: str | None, ) -> bool: """Whether this event's scan is safe to run detached. @@ -403,7 +404,7 @@ def should_detach_scan( def build_ai_guardrails_scan_parameters( ctx: typer.Context, - paths: Optional[tuple[str, ...]], + paths: tuple[str, ...] | None, payload: AIHookPayload, event_type: AiHookEventType, ) -> dict: diff --git a/cycode/cli/apps/ai_guardrails/scan/mcp_server_status.py b/cycode/cli/apps/ai_guardrails/scan/mcp_server_status.py index f6a19c1a..8f05f1b2 100644 --- a/cycode/cli/apps/ai_guardrails/scan/mcp_server_status.py +++ b/cycode/cli/apps/ai_guardrails/scan/mcp_server_status.py @@ -7,7 +7,7 @@ import time from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Optional +from typing import Any from marshmallow import EXCLUDE, Schema, fields, post_load @@ -34,7 +34,7 @@ def get_mcp_server_statuses_cache_path() -> Path: return Path.home() / CYCODE_CONFIGURATION_DIRECTORY / MCP_SERVER_STATUSES_FILE_NAME -def is_enforced(status: Optional[McpServerAuthorizationStatus]) -> bool: +def is_enforced(status: McpServerAuthorizationStatus | None) -> bool: """``status`` is None when the platform never saw the server.""" return status == McpServerAuthorizationStatus.UNAUTHORIZED @@ -43,7 +43,7 @@ def is_enforced(status: Optional[McpServerAuthorizationStatus]) -> bool: class McpServerStatuses: servers: list[McpServerStatus] fetched_at: float - tenant_id: Optional[str] = None + tenant_id: str | None = None ttl_seconds: float = DEFAULT_TTL_SECONDS _by_alias: dict = field(init=False, repr=False) @@ -58,7 +58,7 @@ def __post_init__(self) -> None: if known is None or _RESTRICTIVENESS[server.status] > _RESTRICTIVENESS[known]: self._by_alias[key] = server.status - def status_of(self, alias: str) -> Optional[McpServerAuthorizationStatus]: + def status_of(self, alias: str) -> McpServerAuthorizationStatus | None: return self._by_alias.get(alias.lower()) @@ -78,7 +78,7 @@ def build_dto(self, data: dict[str, Any], **_) -> McpServerStatuses: def save_mcp_server_statuses( - servers: list[McpServerStatus], tenant_id: Optional[str], ttl_seconds: float = DEFAULT_TTL_SECONDS + servers: list[McpServerStatus], tenant_id: str | None, ttl_seconds: float = DEFAULT_TTL_SECONDS ) -> None: path = get_mcp_server_statuses_cache_path() statuses = McpServerStatuses(servers=servers, fetched_at=time.time(), tenant_id=tenant_id, ttl_seconds=ttl_seconds) @@ -89,7 +89,7 @@ def save_mcp_server_statuses( logger.debug('Failed to save MCP server statuses cache', exc_info=e) -def load_mcp_server_statuses() -> Optional[McpServerStatuses]: +def load_mcp_server_statuses() -> McpServerStatuses | None: path = get_mcp_server_statuses_cache_path() if not path.exists(): return None diff --git a/cycode/cli/apps/ai_guardrails/scan/payload.py b/cycode/cli/apps/ai_guardrails/scan/payload.py index b131f6a8..668d878a 100644 --- a/cycode/cli/apps/ai_guardrails/scan/payload.py +++ b/cycode/cli/apps/ai_guardrails/scan/payload.py @@ -7,7 +7,6 @@ import uuid from dataclasses import dataclass, field -from typing import Optional @dataclass @@ -15,9 +14,9 @@ class AIHookPayload: """Unified payload that normalizes field names across IDEs.""" # Event identification - event_name: Optional[str] = None # Canonical event type from AiHookEventType - conversation_id: Optional[str] = None - generation_id: Optional[str] = None + event_name: str | None = None # Canonical event type from AiHookEventType + conversation_id: str | None = None + generation_id: str | None = None # Minted here rather than by the server: the guardrail scan and the hook event are reported in two # separate requests, and both have to name the same event. A generation id can't stand in for it - the @@ -25,16 +24,16 @@ class AIHookPayload: hook_event_id: str = field(default_factory=lambda: str(uuid.uuid4())) # User and IDE information - ide_user_email: Optional[str] = None - model: Optional[str] = None - ide_provider: Optional[str] = None # Matches IDE.name (e.g. 'cursor', 'claude-code') - ide_version: Optional[str] = None + ide_user_email: str | None = None + model: str | None = None + ide_provider: str | None = None # Matches IDE.name (e.g. 'cursor', 'claude-code') + ide_version: str | None = None - source: Optional[str] = None + source: str | None = None # Event-specific data - prompt: Optional[str] = None # PROMPT events - file_path: Optional[str] = None # FILE_READ events - mcp_server_name: Optional[str] = None # MCP_EXECUTION events - mcp_tool_name: Optional[str] = None - mcp_arguments: Optional[dict] = None + prompt: str | None = None # PROMPT events + file_path: str | None = None # FILE_READ events + mcp_server_name: str | None = None # MCP_EXECUTION events + mcp_tool_name: str | None = None + mcp_arguments: dict | None = None diff --git a/cycode/cli/apps/ai_guardrails/scan/policy.py b/cycode/cli/apps/ai_guardrails/scan/policy.py index 79849720..94f97f30 100644 --- a/cycode/cli/apps/ai_guardrails/scan/policy.py +++ b/cycode/cli/apps/ai_guardrails/scan/policy.py @@ -15,7 +15,7 @@ import os import sys from pathlib import Path -from typing import Any, Optional +from typing import Any import yaml @@ -71,7 +71,7 @@ def deep_merge(base: dict, override: dict) -> dict: return result -def load_yaml_file(path: Path) -> Optional[dict]: +def load_yaml_file(path: Path) -> dict | None: """Load a YAML or JSON config file.""" if not path.exists(): return None @@ -101,7 +101,7 @@ def get_policy_value(policy: dict, *keys: str, default: Any = None) -> Any: return current -def load_policy(workspace_root: Optional[str] = None) -> dict: +def load_policy(workspace_root: str | None = None) -> dict: """ Load policy by merging configs in order of precedence. diff --git a/cycode/cli/apps/ai_guardrails/scan/scan_command.py b/cycode/cli/apps/ai_guardrails/scan/scan_command.py index e81d1ae3..23b02424 100644 --- a/cycode/cli/apps/ai_guardrails/scan/scan_command.py +++ b/cycode/cli/apps/ai_guardrails/scan/scan_command.py @@ -7,7 +7,7 @@ ``HookDecision``); ``IDE.build_hook_response`` is the per-IDE translation step. """ -from typing import Annotated, Optional, Union +from typing import Annotated from uuid import uuid4 import click @@ -46,9 +46,9 @@ def _get_auth_error_message(error: Exception) -> str: def _deny_for_event( - event_name: Optional[Union[str, AiHookEventType]], + event_name: str | AiHookEventType | None, user_message: str, - agent_message: Optional[str] = None, + agent_message: str | None = None, ) -> HookDecision: """Build a deny decision matched to ``event_name``'s response shape. @@ -62,7 +62,7 @@ def _deny_for_event( return HookDecision.deny(target, user_message, agent_message) -def _should_skip_payload(ide_integration: IDE, payload: Optional[dict]) -> bool: +def _should_skip_payload(ide_integration: IDE, payload: dict | None) -> bool: """Fast exits that never scan: empty/foreign/synthetic payloads all answer a plain allow.""" if not payload: logger.debug('Empty or invalid JSON payload received') diff --git a/cycode/cli/apps/ai_guardrails/session_start_command.py b/cycode/cli/apps/ai_guardrails/session_start_command.py index a56161b9..62ef2e6c 100644 --- a/cycode/cli/apps/ai_guardrails/session_start_command.py +++ b/cycode/cli/apps/ai_guardrails/session_start_command.py @@ -5,7 +5,7 @@ import sys import time from pathlib import Path -from typing import TYPE_CHECKING, Annotated, Optional +from typing import TYPE_CHECKING, Annotated import typer @@ -56,14 +56,14 @@ def _session_context_digest(report: dict) -> str: return hashlib.sha256(canonical.encode('utf-8')).hexdigest() -def _report_ttl_seconds(config: Optional[GuardrailConfig]) -> float: +def _report_ttl_seconds(config: GuardrailConfig | None) -> float: """The response carries the MCP server statuses, so the guardrail's TTL applies while it is enabled.""" if config is None or config.is_off_for_every_agent(BlockReason.UNAUTHORIZED_MCP_SERVER): return _SESSION_CONTEXT_TTL_SECONDS return config.ttl_seconds -def _should_skip_report(digest: str, tenant_id: Optional[str], ttl_seconds: float) -> bool: +def _should_skip_report(digest: str, tenant_id: str | None, ttl_seconds: float) -> bool: """Skip when the same payload was already sent for this tenant and the TTL hasn't expired.""" try: cache = json.loads(_session_context_cache_path().read_text(encoding='utf-8')) @@ -77,7 +77,7 @@ def _should_skip_report(digest: str, tenant_id: Optional[str], ttl_seconds: floa return False -def _save_report_cache(digest: str, tenant_id: Optional[str]) -> None: +def _save_report_cache(digest: str, tenant_id: str | None) -> None: try: cache_path = _session_context_cache_path() cache_path.parent.mkdir(parents=True, exist_ok=True) @@ -90,9 +90,9 @@ def _save_report_cache(digest: str, tenant_id: Optional[str]) -> None: def _report_session_context( ai_client: 'AISecurityManagerClient', - user_email: Optional[str], - tenant_id: Optional[str], - config: Optional[GuardrailConfig], + user_email: str | None, + tenant_id: str | None, + config: GuardrailConfig | None, ) -> None: """Report the device + cross-IDE session context to the AI security manager. Never raises. @@ -195,7 +195,7 @@ def session_start_command( _report_session_context(ai_client, session_payload.ide_user_email, auth_info.tenant_id, config) -def _sync_guardrail_config(ai_client: 'AISecurityManagerClient', tenant_id: Optional[str]) -> Optional[GuardrailConfig]: +def _sync_guardrail_config(ai_client: 'AISecurityManagerClient', tenant_id: str | None) -> GuardrailConfig | None: """Refresh the guardrail config cache when it is expired or belongs to another tenant. Every step here swallows its own failures - a broken cache or an unreachable platform diff --git a/cycode/cli/apps/ai_guardrails/status_command.py b/cycode/cli/apps/ai_guardrails/status_command.py index da201545..5dc46fe4 100644 --- a/cycode/cli/apps/ai_guardrails/status_command.py +++ b/cycode/cli/apps/ai_guardrails/status_command.py @@ -2,7 +2,7 @@ import os from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer from rich.table import Table @@ -30,7 +30,7 @@ def status_command( ), ] = DEFAULT_IDE_NAME, repo_path: Annotated[ - Optional[Path], + Path | None, typer.Option( '--repo-path', help='Repository path for repo-scoped status (defaults to current directory).', diff --git a/cycode/cli/apps/ai_guardrails/uninstall_command.py b/cycode/cli/apps/ai_guardrails/uninstall_command.py index f9a995f3..725110f2 100644 --- a/cycode/cli/apps/ai_guardrails/uninstall_command.py +++ b/cycode/cli/apps/ai_guardrails/uninstall_command.py @@ -1,7 +1,7 @@ """Uninstall command for AI guardrails hooks.""" from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer @@ -28,7 +28,7 @@ def uninstall_command( ), ] = DEFAULT_IDE_NAME, repo_path: Annotated[ - Optional[Path], + Path | None, typer.Option( '--repo-path', help='Repository path for repo-scoped uninstallation (defaults to current directory).', diff --git a/cycode/cli/apps/api/__init__.py b/cycode/cli/apps/api/__init__.py index e65f9c6f..a7f89a20 100644 --- a/cycode/cli/apps/api/__init__.py +++ b/cycode/cli/apps/api/__init__.py @@ -5,7 +5,7 @@ and cached locally for 24 hours. """ -from typing import Any, Optional +from typing import Any import click @@ -32,7 +32,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._loaded: bool = False - def _ensure_loaded(self, ctx: Optional[click.Context]) -> None: + def _ensure_loaded(self, ctx: click.Context | None) -> None: if self._loaded: return self._loaded = True # set first to avoid re-entrancy on errors @@ -59,7 +59,7 @@ def list_commands(self, ctx: click.Context) -> list[str]: self._ensure_loaded(ctx) return super().list_commands(ctx) - def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]: + def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None: self._ensure_loaded(ctx) return super().get_command(ctx, cmd_name) diff --git a/cycode/cli/apps/api/api_command.py b/cycode/cli/apps/api/api_command.py index 1926c93c..ae096d1b 100644 --- a/cycode/cli/apps/api/api_command.py +++ b/cycode/cli/apps/api/api_command.py @@ -2,7 +2,7 @@ import json import re -from typing import Any, Optional +from typing import Any import click @@ -60,7 +60,7 @@ def _path_to_command_name(path: str, common_prefix: str, has_path_params: bool) /v4/violations/count (prefix=/v4/violations) -> count """ # Strip common prefix - relative = path[len(common_prefix) :] if path.startswith(common_prefix) else path + relative = path.removeprefix(common_prefix) relative = relative.strip('/') # Remove path parameter segments and empty parts @@ -91,8 +91,8 @@ def _make_api_request( method: str, path_params: dict[str, str], query_params: dict[str, Any], - client_id: Optional[str] = None, - client_secret: Optional[str] = None, + client_id: str | None = None, + client_secret: str | None = None, ) -> dict: """Execute an API request using the CLI's standard auth client.""" from urllib.parse import quote @@ -115,8 +115,8 @@ def _make_api_request( def build_api_command_groups( - client_id: Optional[str] = None, - client_secret: Optional[str] = None, + client_id: str | None = None, + client_secret: str | None = None, ) -> list[tuple[click.Group, str]]: """Build Click command groups from the OpenAPI spec. diff --git a/cycode/cli/apps/api/openapi_spec.py b/cycode/cli/apps/api/openapi_spec.py index 74ffdb69..529935fe 100644 --- a/cycode/cli/apps/api/openapi_spec.py +++ b/cycode/cli/apps/api/openapi_spec.py @@ -4,7 +4,6 @@ import os import time from pathlib import Path -from typing import Optional from cycode.cli.consts import CYCODE_CONFIGURATION_DIRECTORY from cycode.cli.user_settings.credentials_manager import CredentialsManager @@ -20,7 +19,7 @@ _OPENAPI_SPEC_PATH = '/v4/api-docs/cycode-api-swagger.json' -def get_openapi_spec(client_id: Optional[str] = None, client_secret: Optional[str] = None) -> dict: +def get_openapi_spec(client_id: str | None = None, client_secret: str | None = None) -> dict: """Get the OpenAPI spec, using cache if fresh, otherwise fetching from API. The spec is only fetched when the user actually invokes `cycode platform ...`. @@ -45,7 +44,7 @@ def get_openapi_spec(client_id: Optional[str] = None, client_secret: Optional[st return _fetch_and_cache_spec(client_id, client_secret) -def _load_cached_spec() -> Optional[dict]: +def _load_cached_spec() -> dict | None: """Load spec from local cache if it exists and is fresh.""" if not _CACHE_FILE.exists(): return None @@ -64,7 +63,7 @@ def _load_cached_spec() -> Optional[dict]: return None -def resolve_credentials(client_id: Optional[str] = None, client_secret: Optional[str] = None) -> tuple[str, str]: +def resolve_credentials(client_id: str | None = None, client_secret: str | None = None) -> tuple[str, str]: """Resolve credentials from args or the CLI's standard credential chain.""" if not client_id or not client_secret: credentials_manager = CredentialsManager() @@ -81,7 +80,7 @@ def resolve_credentials(client_id: Optional[str] = None, client_secret: Optional return client_id, client_secret -def _fetch_and_cache_spec(client_id: Optional[str] = None, client_secret: Optional[str] = None) -> dict: +def _fetch_and_cache_spec(client_id: str | None = None, client_secret: str | None = None) -> dict: """Fetch OpenAPI spec from API and cache to disk. Uses CycodeTokenBasedClient for auth and retries. The spec is served from the app URL, diff --git a/cycode/cli/apps/auth/auth_common.py b/cycode/cli/apps/auth/auth_common.py index f4ea09d9..a3cf2b94 100644 --- a/cycode/cli/apps/auth/auth_common.py +++ b/cycode/cli/apps/auth/auth_common.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from cycode.cli.apps.auth.models import AuthInfo from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, RequestHttpError @@ -11,7 +11,7 @@ from typer import Context -def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]: +def get_authorization_info(ctx: 'Context') -> AuthInfo | None: printer = ctx.obj.get('console_printer') client_id = ctx.obj.get('client_id') @@ -50,8 +50,8 @@ def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]: def _try_oidc_authorization( - ctx: 'Context', printer: any, client_id: Optional[str], id_token: Optional[str] -) -> Optional[AuthInfo]: + ctx: 'Context', printer: any, client_id: str | None, id_token: str | None +) -> AuthInfo | None: if not client_id or not id_token: return None diff --git a/cycode/cli/apps/configure/configure_command.py b/cycode/cli/apps/configure/configure_command.py index 3c2f269b..871ddb86 100644 --- a/cycode/cli/apps/configure/configure_command.py +++ b/cycode/cli/apps/configure/configure_command.py @@ -1,5 +1,3 @@ -from typing import Optional - from cycode.cli.apps.configure.consts import CONFIGURATION_MANAGER, CREDENTIALS_MANAGER from cycode.cli.apps.configure.messages import ( get_credentials_environment_variables_override_warning, @@ -18,8 +16,8 @@ def _should_update_value( - old_value: Optional[str], - new_value: Optional[str], + old_value: str | None, + new_value: str | None, ) -> bool: if not new_value: return False diff --git a/cycode/cli/apps/configure/messages.py b/cycode/cli/apps/configure/messages.py index f008f09d..d81bb21c 100644 --- a/cycode/cli/apps/configure/messages.py +++ b/cycode/cli/apps/configure/messages.py @@ -1,5 +1,3 @@ -from typing import Optional - from cycode.cli.apps.configure.consts import ( CONFIGURATION_MANAGER, CREDENTIALS_ARE_SET_IN_ENVIRONMENT_VARIABLES_MESSAGE, @@ -19,7 +17,7 @@ def get_credentials_update_result_message() -> str: return CREDENTIALS_UPDATED_SUCCESSFULLY_MESSAGE.format(filename=CREDENTIALS_MANAGER.get_filename()) -def get_credentials_environment_variables_override_warning() -> Optional[str]: +def get_credentials_environment_variables_override_warning() -> str | None: if _are_credentials_exist_in_environment_variables(): return CREDENTIALS_ARE_SET_IN_ENVIRONMENT_VARIABLES_MESSAGE @@ -38,7 +36,7 @@ def get_urls_update_result_message() -> str: ) -def get_urls_environment_variables_override_warning() -> Optional[str]: +def get_urls_environment_variables_override_warning() -> str | None: if _are_urls_exist_in_environment_variables(): return URLS_ARE_SET_IN_ENVIRONMENT_VARIABLES_MESSAGE diff --git a/cycode/cli/apps/configure/prompts.py b/cycode/cli/apps/configure/prompts.py index 63fae12c..5106fc5f 100644 --- a/cycode/cli/apps/configure/prompts.py +++ b/cycode/cli/apps/configure/prompts.py @@ -1,12 +1,10 @@ -from typing import Optional - import typer from cycode.cli import consts from cycode.cli.utils.string_utils import obfuscate_text -def get_client_id_input(current_client_id: Optional[str]) -> Optional[str]: +def get_client_id_input(current_client_id: str | None) -> str | None: prompt_text = 'Cycode Client ID' prompt_suffix = ' []: ' @@ -17,7 +15,7 @@ def get_client_id_input(current_client_id: Optional[str]) -> Optional[str]: return new_client_id or current_client_id -def get_client_secret_input(current_client_secret: Optional[str]) -> Optional[str]: +def get_client_secret_input(current_client_secret: str | None) -> str | None: prompt_text = 'Cycode Client Secret' prompt_suffix = ' []: ' @@ -28,7 +26,7 @@ def get_client_secret_input(current_client_secret: Optional[str]) -> Optional[st return new_client_secret or current_client_secret -def get_app_url_input(current_app_url: Optional[str]) -> str: +def get_app_url_input(current_app_url: str | None) -> str: prompt_text = 'Cycode APP URL' default = consts.DEFAULT_CYCODE_APP_URL @@ -38,7 +36,7 @@ def get_app_url_input(current_app_url: Optional[str]) -> str: return typer.prompt(text=prompt_text, default=default, type=str) -def get_api_url_input(current_api_url: Optional[str]) -> str: +def get_api_url_input(current_api_url: str | None) -> str: prompt_text = 'Cycode API URL' default = consts.DEFAULT_CYCODE_API_URL @@ -48,7 +46,7 @@ def get_api_url_input(current_api_url: Optional[str]) -> str: return typer.prompt(text=prompt_text, default=default, type=str) -def get_id_token_input(current_id_token: Optional[str]) -> Optional[str]: +def get_id_token_input(current_id_token: str | None) -> str | None: prompt_text = 'Cycode ID Token' prompt_suffix = ' []: ' diff --git a/cycode/cli/apps/ignore/ignore_command.py b/cycode/cli/apps/ignore/ignore_command.py index c65197c3..b4815539 100644 --- a/cycode/cli/apps/ignore/ignore_command.py +++ b/cycode/cli/apps/ignore/ignore_command.py @@ -1,5 +1,5 @@ import re -from typing import Annotated, Optional +from typing import Annotated import click import typer @@ -22,7 +22,7 @@ def _is_package_pattern_valid(package: str) -> bool: def ignore_command( # noqa: C901 by_path: Annotated[ - Optional[str], + str | None, typer.Option( help='Ignore a specific file or directory while scanning.', show_default=False, @@ -30,7 +30,7 @@ def ignore_command( # noqa: C901 ), ] = None, by_rule: Annotated[ - Optional[str], + str | None, typer.Option( help='Ignore scanning a specific Secrets rule ID or IaC rule ID.', show_default=False, @@ -38,7 +38,7 @@ def ignore_command( # noqa: C901 ), ] = None, by_value: Annotated[ - Optional[str], + str | None, typer.Option( help='Ignore a specific value.', show_default=False, @@ -46,7 +46,7 @@ def ignore_command( # noqa: C901 ), ] = None, by_sha: Annotated[ - Optional[str], + str | None, typer.Option( help='Ignore a specific SHA512 representation of a string.', show_default=False, @@ -54,7 +54,7 @@ def ignore_command( # noqa: C901 ), ] = None, by_package: Annotated[ - Optional[str], + str | None, typer.Option( help='Ignore scanning a specific package version. Expected pattern: [cyan]name@version[/].', show_default=False, @@ -62,7 +62,7 @@ def ignore_command( # noqa: C901 ), ] = None, by_cve: Annotated[ - Optional[str], + str | None, typer.Option( help='Ignore scanning a specific CVE. Expected pattern: [cyan]CVE-YYYY-NNN[/].', show_default=False, diff --git a/cycode/cli/apps/mcp/mcp_command.py b/cycode/cli/apps/mcp/mcp_command.py index 517f514f..95568f34 100644 --- a/cycode/cli/apps/mcp/mcp_command.py +++ b/cycode/cli/apps/mcp/mcp_command.py @@ -6,7 +6,7 @@ import sys import tempfile import uuid -from typing import Annotated, Any, Optional +from typing import Annotated, Any import anyio import typer @@ -229,8 +229,8 @@ def _build_scan_summary(result: dict[str, Any]) -> str: async def _cycode_scan_tool( scan_type: ScanTypeOption, - files: Optional[dict[str, str]] = None, - paths: Optional[list[str]] = None, + files: dict[str, str] | None = None, + paths: list[str] | None = None, ) -> str: _tool_call_id = _gen_random_id() _logger.info('Scan tool called, %s', {'scan_type': scan_type, 'call_id': _tool_call_id}) @@ -288,8 +288,8 @@ async def _cycode_scan_tool( async def cycode_secret_scan( - paths: Optional[list[str]] = _PATHS_TOOL_FIELD, - files: Optional[dict[str, str]] = _FILES_TOOL_FIELD, + paths: list[str] | None = _PATHS_TOOL_FIELD, + files: dict[str, str] | None = _FILES_TOOL_FIELD, ) -> str: """Scan for hardcoded secrets. @@ -309,8 +309,8 @@ async def cycode_secret_scan( async def cycode_sca_scan( - paths: Optional[list[str]] = _PATHS_TOOL_FIELD, - files: Optional[dict[str, str]] = _FILES_TOOL_FIELD, + paths: list[str] | None = _PATHS_TOOL_FIELD, + files: dict[str, str] | None = _FILES_TOOL_FIELD, ) -> str: """Scan for Software Composition Analysis (SCA) - vulnerabilities and license issues. @@ -337,8 +337,8 @@ async def cycode_sca_scan( async def cycode_iac_scan( - paths: Optional[list[str]] = _PATHS_TOOL_FIELD, - files: Optional[dict[str, str]] = _FILES_TOOL_FIELD, + paths: list[str] | None = _PATHS_TOOL_FIELD, + files: dict[str, str] | None = _FILES_TOOL_FIELD, ) -> str: """Scan for Infrastructure as Code (IaC) misconfigurations. @@ -360,8 +360,8 @@ async def cycode_iac_scan( async def cycode_sast_scan( - paths: Optional[list[str]] = _PATHS_TOOL_FIELD, - files: Optional[dict[str, str]] = _FILES_TOOL_FIELD, + paths: list[str] | None = _PATHS_TOOL_FIELD, + files: dict[str, str] | None = _FILES_TOOL_FIELD, ) -> str: """Scan for Static Application Security Testing (SAST) - code quality and security flaws. diff --git a/cycode/cli/apps/report/sbom/common.py b/cycode/cli/apps/report/sbom/common.py index 067a9fa6..b197bf03 100644 --- a/cycode/cli/apps/report/sbom/common.py +++ b/cycode/cli/apps/report/sbom/common.py @@ -1,7 +1,7 @@ import pathlib import time from platform import platform -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from cycode.cli import consts from cycode.cli.apps.report.sbom.sbom_report_file import SbomReportFile @@ -20,7 +20,7 @@ def _poll_report_execution_until_completed( progress_bar: 'BaseProgressBar', client: 'ReportClient', report_execution_id: int, - polling_timeout: Optional[int] = None, + polling_timeout: int | None = None, ) -> ReportExecutionSchema: if polling_timeout is None: polling_timeout = configuration_manager.get_report_polling_timeout_in_seconds() @@ -50,8 +50,8 @@ def send_report_feedback( report_command_type: str, request_report_parameters: dict, report_execution_id: int, - error_message: Optional[str] = None, - request_zip_file_size: Optional[int] = None, + error_message: str | None = None, + request_zip_file_size: int | None = None, **kwargs, ) -> None: try: @@ -77,7 +77,7 @@ def create_sbom_report( progress_bar: 'BaseProgressBar', client: 'ReportClient', report_execution_id: int, - output_file: Optional[pathlib.Path], + output_file: pathlib.Path | None, output_format: str, ) -> None: report_execution = _poll_report_execution_until_completed(progress_bar, client, report_execution_id) diff --git a/cycode/cli/apps/report/sbom/sbom_command.py b/cycode/cli/apps/report/sbom/sbom_command.py index 4454a966..bcd3d3a2 100644 --- a/cycode/cli/apps/report/sbom/sbom_command.py +++ b/cycode/cli/apps/report/sbom/sbom_command.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import click import typer @@ -32,7 +32,7 @@ def sbom_command( ), ] = SbomOutputFormatOption.JSON, output_file: Annotated[ - Optional[Path], + Path | None, typer.Option( help='Output file.', show_default='Autogenerated filename saved to the current directory', diff --git a/cycode/cli/apps/report/sbom/sbom_report_file.py b/cycode/cli/apps/report/sbom/sbom_report_file.py index f3178b44..9eabf02d 100644 --- a/cycode/cli/apps/report/sbom/sbom_report_file.py +++ b/cycode/cli/apps/report/sbom/sbom_report_file.py @@ -1,7 +1,6 @@ import os import pathlib import re -from typing import Optional import typer @@ -9,7 +8,7 @@ class SbomReportFile: - def __init__(self, storage_path: str, output_format: str, output_file: Optional[pathlib.Path]) -> None: + def __init__(self, storage_path: str, output_format: str, output_file: pathlib.Path | None) -> None: if output_file is None: output_file = pathlib.Path(storage_path) diff --git a/cycode/cli/apps/report_import/sbom/sbom_command.py b/cycode/cli/apps/report_import/sbom/sbom_command.py index b6b5dfeb..49dce818 100644 --- a/cycode/cli/apps/report_import/sbom/sbom_command.py +++ b/cycode/cli/apps/report_import/sbom/sbom_command.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer @@ -24,13 +24,13 @@ def sbom_command( str, typer.Option('--vendor', '-v', help='Vendor Name.', case_sensitive=False, show_default=False) ], labels: Annotated[ - Optional[list[str]], + list[str] | None, typer.Option( '--label', '-l', help='Label, can be specified multiple times.', case_sensitive=False, show_default=False ), ] = None, owners: Annotated[ - Optional[list[str]], + list[str] | None, typer.Option( '--owner', '-o', diff --git a/cycode/cli/apps/sca_options.py b/cycode/cli/apps/sca_options.py index 01def411..ad48b5a4 100644 --- a/cycode/cli/apps/sca_options.py +++ b/cycode/cli/apps/sca_options.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer @@ -25,7 +25,7 @@ ] MavenSettingsFileOption = Annotated[ - Optional[Path], + Path | None, typer.Option( '--maven-settings-file', show_default=False, @@ -49,7 +49,7 @@ def apply_sca_restore_options_to_context( ctx: typer.Context, no_restore: bool, gradle_all_sub_projects: bool, - maven_settings_file: Optional[Path], + maven_settings_file: Path | None, stop_on_error: bool = False, ) -> None: ctx.obj['no_restore'] = no_restore diff --git a/cycode/cli/apps/scan/aggregation_report.py b/cycode/cli/apps/scan/aggregation_report.py index 45b891ed..583eb1cc 100644 --- a/cycode/cli/apps/scan/aggregation_report.py +++ b/cycode/cli/apps/scan/aggregation_report.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING import typer @@ -10,13 +10,13 @@ logger = get_logger('Aggregation Report URL') -def _set_aggregation_report_url(ctx: typer.Context, aggregation_report_url: Optional[str] = None) -> None: +def _set_aggregation_report_url(ctx: typer.Context, aggregation_report_url: str | None = None) -> None: ctx.obj['aggregation_report_url'] = aggregation_report_url def try_get_aggregation_report_url_if_needed( scan_parameters: dict, cycode_client: 'ScanClient', scan_type: str -) -> Optional[str]: +) -> str | None: if not scan_parameters.get('report', False): return None @@ -37,6 +37,6 @@ def try_set_aggregation_report_url_if_needed( aggregation_report_url = try_get_aggregation_report_url_if_needed(scan_parameters, cycode_client, scan_type) if aggregation_report_url: _set_aggregation_report_url(ctx, aggregation_report_url) - logger.debug('Aggregation report URL set successfully', {'aggregation_report_url': aggregation_report_url}) + logger.debug('Aggregation report URL set successfully, %s', {'aggregation_report_url': aggregation_report_url}) else: logger.debug('No aggregation report URL found or report generation is disabled') diff --git a/cycode/cli/apps/scan/code_scanner.py b/cycode/cli/apps/scan/code_scanner.py index 77af5ea6..8c321c56 100644 --- a/cycode/cli/apps/scan/code_scanner.py +++ b/cycode/cli/apps/scan/code_scanner.py @@ -1,8 +1,9 @@ import os import time import zipfile +from collections.abc import Callable from platform import platform -from typing import TYPE_CHECKING, Callable, Optional +from typing import TYPE_CHECKING import requests import typer @@ -147,7 +148,7 @@ def _get_scan_documents_thread_func( is_git_diff: bool, is_commit_range: bool, scan_parameters: dict, - prezipped: Optional['InMemoryZip'] = None, + prezipped: 'InMemoryZip | None' = None, ) -> Callable[[list[Document]], tuple[str, CliError, LocalScanResult]]: cycode_client = ctx.obj['client'] scan_type = ctx.obj['scan_type'] @@ -363,7 +364,7 @@ def _perform_scan_v4_async( scan_parameters: dict, is_git_diff: bool, is_commit_range: bool, - on_upload_progress: Optional[Callable] = None, + on_upload_progress: Callable | None = None, ) -> ZippedFileScanResult: upload_link = cycode_client.get_upload_link(scan_type) logger.debug('Got upload link, %s', {'upload_id': upload_link.upload_id}) @@ -390,7 +391,7 @@ def _perform_scan_async( scan_type: str, scan_parameters: dict, is_commit_range: bool, - on_upload_progress: Optional[Callable] = None, + on_upload_progress: Callable | None = None, ) -> ZippedFileScanResult: scan_async_result = cycode_client.zipped_file_scan_async( zipped_documents, @@ -429,7 +430,7 @@ def _perform_scan( is_commit_range: bool, scan_parameters: dict, should_use_sync_flow: bool = False, - on_upload_progress: Optional[Callable] = None, + on_upload_progress: Callable | None = None, ) -> ZippedFileScanResult: if should_use_sync_flow: # it does not support commit range scans; should_use_sync_flow handles it @@ -463,7 +464,7 @@ def poll_scan_results( scan_id: str, scan_type: str, scan_parameters: dict, - polling_timeout: Optional[int] = None, + polling_timeout: int | None = None, ) -> 'ZippedFileScanResult': if polling_timeout is None: polling_timeout = configuration_manager.get_scan_polling_timeout_in_seconds() @@ -504,7 +505,7 @@ def report_scan_status( files_to_scan_count: int, zip_size: int, command_scan_type: str, - error_message: Optional[str], + error_message: str | None, ) -> None: try: end_scan_time = time.time() diff --git a/cycode/cli/apps/scan/commit_range_scanner.py b/cycode/cli/apps/scan/commit_range_scanner.py index f1f52318..ef2825de 100644 --- a/cycode/cli/apps/scan/commit_range_scanner.py +++ b/cycode/cli/apps/scan/commit_range_scanner.py @@ -1,5 +1,5 @@ import os -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING import click import requests @@ -72,7 +72,7 @@ def _perform_commit_range_scan_async( to_commit_zipped_documents: 'InMemoryZip', scan_type: str, scan_parameters: dict, - timeout: Optional[int] = None, + timeout: int | None = None, ) -> ZippedFileScanResult: scan_async_result = cycode_client.commit_range_scan_async( from_commit_zipped_documents, to_commit_zipped_documents, scan_type, scan_parameters @@ -90,7 +90,7 @@ def _perform_commit_range_scan_v4_async( to_commit_zipped_documents: 'InMemoryZip', scan_type: str, scan_parameters: dict, - timeout: Optional[int] = None, + timeout: int | None = None, ) -> ZippedFileScanResult: from_upload_link = cycode_client.get_upload_link(scan_type) logger.debug('Got from-commit upload link, %s', {'upload_id': from_upload_link.upload_id}) @@ -120,8 +120,8 @@ def _scan_commit_range_documents( ctx: typer.Context, from_documents_to_scan: list[Document], to_documents_to_scan: list[Document], - scan_parameters: Optional[dict] = None, - timeout: Optional[int] = None, + scan_parameters: dict | None = None, + timeout: int | None = None, ) -> None: cycode_client = ctx.obj['client'] scan_type = ctx.obj['scan_type'] @@ -265,7 +265,7 @@ def _scan_sca_commit_range(ctx: typer.Context, repo_path: str, commit_range: str def _scan_secret_commit_range( - ctx: typer.Context, repo_path: str, commit_range: str, max_commits_count: Optional[int] = None + ctx: typer.Context, repo_path: str, commit_range: str, max_commits_count: int | None = None ) -> None: commit_diff_documents_to_scan = collect_commit_range_diff_documents(ctx, repo_path, commit_range, max_commits_count) diff_documents_to_scan = excluder.exclude_irrelevant_documents_to_scan( @@ -329,7 +329,7 @@ def _scan_sca_pre_commit( repo_path: str, base_ref: str = consts.GIT_HEAD_COMMIT_REV, include_unstaged: bool = False, - paths: Optional[list[str]] = None, + paths: list[str] | None = None, ) -> None: scan_parameters = get_scan_parameters(ctx, (repo_path,)) @@ -367,7 +367,7 @@ def _scan_secret_pre_commit( repo_path: str, base_ref: str = consts.GIT_HEAD_COMMIT_REV, include_unstaged: bool = False, - paths: Optional[list[str]] = None, + paths: list[str] | None = None, ) -> None: # collect_file_contents=False: the secret scan only ever uses diff_documents below, so skip # building from_ref_documents/working_copy_documents (a disk read per changed file it would @@ -395,7 +395,7 @@ def _scan_sast_pre_commit( repo_path: str, base_ref: str = consts.GIT_HEAD_COMMIT_REV, include_unstaged: bool = False, - paths: Optional[list[str]] = None, + paths: list[str] | None = None, **_, ) -> None: scan_parameters = get_scan_parameters(ctx, (repo_path,)) @@ -435,7 +435,7 @@ def scan_pre_commit( repo_path: str, base_ref: str = consts.GIT_HEAD_COMMIT_REV, include_unstaged: bool = False, - paths: Optional[list[str]] = None, + paths: list[str] | None = None, ) -> None: scan_type = ctx.obj['scan_type'] if scan_type not in _SCAN_TYPE_TO_PRE_COMMIT_HANDLER: diff --git a/cycode/cli/apps/scan/detection_excluder.py b/cycode/cli/apps/scan/detection_excluder.py index 3697bcaf..9c971c99 100644 --- a/cycode/cli/apps/scan/detection_excluder.py +++ b/cycode/cli/apps/scan/detection_excluder.py @@ -1,5 +1,3 @@ -from typing import Optional - from cycode.cli import consts from cycode.cli.cli_types import SeverityOption from cycode.cli.config import configuration_manager @@ -116,7 +114,7 @@ def _is_detection_sha_configured_in_exclusions(detection: Detection, exclusions: return detection_sha in exclusions -def _get_package_name(detection: Detection) -> Optional[str]: +def _get_package_name(detection: Detection) -> str | None: package_name = detection.detection_details.get('vulnerable_component') package_version = detection.detection_details.get('vulnerable_component_version') @@ -130,7 +128,7 @@ def _get_package_name(detection: Detection) -> Optional[str]: return None -def _get_cve_identifier(detection: Detection) -> Optional[str]: +def _get_cve_identifier(detection: Detection) -> str | None: return detection.detection_details.get('alert', {}).get('cve_identifier') diff --git a/cycode/cli/apps/scan/pre_commit/pre_commit_command.py b/cycode/cli/apps/scan/pre_commit/pre_commit_command.py index beacf734..45a2c30c 100644 --- a/cycode/cli/apps/scan/pre_commit/pre_commit_command.py +++ b/cycode/cli/apps/scan/pre_commit/pre_commit_command.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import typer @@ -47,7 +47,7 @@ def _validate_base_ref(repo_path: str, base_ref: str) -> None: def pre_commit_command( ctx: typer.Context, - _: Annotated[Optional[list[str]], typer.Argument(help='Ignored arguments', hidden=True)] = None, + _: Annotated[list[str] | None, typer.Argument(help='Ignored arguments', hidden=True)] = None, base_ref: Annotated[ str, typer.Option( @@ -66,7 +66,7 @@ def pre_commit_command( ), ] = False, paths: Annotated[ - Optional[list[Path]], + list[Path] | None, typer.Option( '--path', help='Optional paths to scope the diff scan to (e.g. the file currently open in an IDE). ' diff --git a/cycode/cli/apps/scan/pre_push/pre_push_command.py b/cycode/cli/apps/scan/pre_push/pre_push_command.py index 3d2801bc..552e2bdb 100644 --- a/cycode/cli/apps/scan/pre_push/pre_push_command.py +++ b/cycode/cli/apps/scan/pre_push/pre_push_command.py @@ -1,6 +1,6 @@ import logging import os -from typing import Annotated, Optional +from typing import Annotated import typer @@ -28,7 +28,7 @@ def pre_push_command( ctx: typer.Context, - _: Annotated[Optional[list[str]], typer.Argument(help='Ignored arguments', hidden=True)] = None, + _: Annotated[list[str] | None, typer.Argument(help='Ignored arguments', hidden=True)] = None, ) -> None: try: if should_skip_pre_receive_scan(): @@ -63,7 +63,7 @@ def pre_push_command( handle_scan_exception(ctx, e) -def _get_pre_push_commit_range() -> Optional[str]: +def _get_pre_push_commit_range() -> str | None: commit_range = get_pre_commit_framework_push_range() if commit_range: logger.debug('Using push details from the pre-commit framework, %s', {'commit_range': commit_range}) diff --git a/cycode/cli/apps/scan/pre_receive/pre_receive_command.py b/cycode/cli/apps/scan/pre_receive/pre_receive_command.py index 70abd4aa..e6d4c91c 100644 --- a/cycode/cli/apps/scan/pre_receive/pre_receive_command.py +++ b/cycode/cli/apps/scan/pre_receive/pre_receive_command.py @@ -1,6 +1,6 @@ import logging import os -from typing import Annotated, Optional +from typing import Annotated import typer @@ -25,7 +25,7 @@ def pre_receive_command( ctx: typer.Context, - _: Annotated[Optional[list[str]], typer.Argument(help='Ignored arguments', hidden=True)] = None, + _: Annotated[list[str] | None, typer.Argument(help='Ignored arguments', hidden=True)] = None, ) -> None: try: if should_skip_pre_receive_scan(): diff --git a/cycode/cli/apps/scan/remote_url_resolver.py b/cycode/cli/apps/scan/remote_url_resolver.py index 870115e2..5d8708d7 100644 --- a/cycode/cli/apps/scan/remote_url_resolver.py +++ b/cycode/cli/apps/scan/remote_url_resolver.py @@ -1,5 +1,3 @@ -from typing import Optional - from cycode.cli import consts from cycode.cli.utils.git_proxy import git_proxy from cycode.cli.utils.shell_executor import shell @@ -9,7 +7,7 @@ logger = get_logger('Remote URL Resolver') -def _get_plastic_repository_name(path: str) -> Optional[str]: +def _get_plastic_repository_name(path: str) -> str | None: """Get the name of the Plastic repository from the current working directory. The command to execute is: @@ -45,7 +43,7 @@ def _get_plastic_repository_name(path: str) -> Optional[str]: return None -def _get_plastic_repository_list(working_dir: Optional[str] = None) -> dict[str, str]: +def _get_plastic_repository_list(working_dir: str | None = None) -> dict[str, str]: """Get the list of Plastic repositories and their GUIDs. The command to execute is: @@ -84,7 +82,7 @@ def _get_plastic_repository_list(working_dir: Optional[str] = None) -> dict[str, return repo_name_to_guid -def _try_to_get_plastic_remote_url(path: str) -> Optional[str]: +def _try_to_get_plastic_remote_url(path: str) -> str | None: repository_name = _get_plastic_repository_name(path) if not repository_name: return None @@ -98,7 +96,7 @@ def _try_to_get_plastic_remote_url(path: str) -> Optional[str]: return f'{consts.PLASTIC_VCS_REMOTE_URI_PREFIX}{repository_guid}' -def _try_get_git_remote_url(path: str) -> Optional[str]: +def _try_get_git_remote_url(path: str) -> str | None: try: repo = git_proxy.get_repo(path, search_parent_directories=True) remote_url = repo.remotes[0].config_reader.get('url') @@ -113,7 +111,7 @@ def _try_get_git_remote_url(path: str) -> Optional[str]: return None -def _try_get_any_remote_url(path: str) -> Optional[str]: +def _try_get_any_remote_url(path: str) -> str | None: remote_url = _try_get_git_remote_url(path) if not remote_url: remote_url = _try_to_get_plastic_remote_url(path) @@ -121,7 +119,7 @@ def _try_get_any_remote_url(path: str) -> Optional[str]: return remote_url -def get_remote_url_scan_parameter(paths: tuple[str, ...]) -> Optional[str]: +def get_remote_url_scan_parameter(paths: tuple[str, ...]) -> str | None: remote_urls = set() for path in paths: # FIXME(MarshalX): perf issue. This looping will produce: diff --git a/cycode/cli/apps/scan/repository/repository_command.py b/cycode/cli/apps/scan/repository/repository_command.py index e32fec0d..0c32c68f 100644 --- a/cycode/cli/apps/scan/repository/repository_command.py +++ b/cycode/cli/apps/scan/repository/repository_command.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import click import typer @@ -25,7 +25,7 @@ def repository_command( Path, typer.Argument(exists=True, resolve_path=True, help='Path to Git repository to scan.', show_default=False) ], branch: Annotated[ - Optional[str], typer.Option('--branch', '-b', help='Branch to scan.', show_default='default branch') + str | None, typer.Option('--branch', '-b', help='Branch to scan.', show_default='default branch') ] = None, ) -> None: try: diff --git a/cycode/cli/apps/scan/scan_command.py b/cycode/cli/apps/scan/scan_command.py index 747cfc69..832064fb 100644 --- a/cycode/cli/apps/scan/scan_command.py +++ b/cycode/cli/apps/scan/scan_command.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import Annotated, Optional +from typing import Annotated import click import typer @@ -121,7 +121,7 @@ def scan_command( ), ] = None, export_file: Annotated[ - Optional[Path], + Path | None, typer.Option( '--export-file', help='Export file. Path to the file where the export will be saved.', diff --git a/cycode/cli/apps/scan/scan_parameters.py b/cycode/cli/apps/scan/scan_parameters.py index d297010a..e57e763c 100644 --- a/cycode/cli/apps/scan/scan_parameters.py +++ b/cycode/cli/apps/scan/scan_parameters.py @@ -1,5 +1,3 @@ -from typing import Optional - import typer from cycode import _BOOT_WALL @@ -23,7 +21,7 @@ def _get_default_scan_parameters(ctx: typer.Context) -> dict: } -def get_scan_parameters(ctx: typer.Context, paths: Optional[tuple[str, ...]] = None) -> dict: +def get_scan_parameters(ctx: typer.Context, paths: tuple[str, ...] | None = None) -> dict: scan_parameters = _get_default_scan_parameters(ctx) if not paths: diff --git a/cycode/cli/apps/scan/scan_result.py b/cycode/cli/apps/scan/scan_result.py index ea598a12..0dcaa476 100644 --- a/cycode/cli/apps/scan/scan_result.py +++ b/cycode/cli/apps/scan/scan_result.py @@ -1,5 +1,5 @@ import os -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING import typer @@ -26,8 +26,8 @@ def _get_document_by_file_name( - documents: list[Document], file_name: str, unique_id: Optional[str] = None -) -> Optional[Document]: + documents: list[Document], file_name: str, unique_id: str | None = None +) -> Document | None: normalized_file_name = normalize_file_path(file_name) for document in documents: if normalize_file_path(document.path) == normalized_file_name and document.unique_id == unique_id: @@ -132,7 +132,7 @@ def _map_detections_per_file_and_commit_id(scan_type: str, raw_detections: list[ file_name = _get_file_name_from_detection(scan_type, raw_detection) detection: Detection = DetectionSchema().load(raw_detection) - commit_id: Optional[str] = detection.detection_details.get('commit_id') # could be None + commit_id: str | None = detection.detection_details.get('commit_id') # could be None group_by_key = (file_name, commit_id) if group_by_key in detections_per_files: @@ -187,7 +187,7 @@ def get_sync_scan_result(scan_type: str, scan_results: 'ScanResultsSyncFlow') -> def print_local_scan_results( - ctx: typer.Context, local_scan_results: list[LocalScanResult], errors: Optional[dict[str, 'CliError']] = None + ctx: typer.Context, local_scan_results: list[LocalScanResult], errors: dict[str, 'CliError'] | None = None ) -> None: printer = ctx.obj.get('console_printer') printer.update_ctx(ctx) diff --git a/cycode/cli/console.py b/cycode/cli/console.py index 5d78fc36..2a04c046 100644 --- a/cycode/cli/console.py +++ b/cycode/cli/console.py @@ -1,5 +1,5 @@ import os -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from rich.console import Console, RenderResult from rich.markdown import Heading, Markdown @@ -14,7 +14,7 @@ console = console_out # alias -def is_dark_console() -> Optional[bool]: +def is_dark_console() -> bool | None: """Detect if the console is dark or light. This function checks the environment variables and terminal type to determine if the console is dark or light. diff --git a/cycode/cli/exceptions/custom_exceptions.py b/cycode/cli/exceptions/custom_exceptions.py index af66e89b..15f700aa 100644 --- a/cycode/cli/exceptions/custom_exceptions.py +++ b/cycode/cli/exceptions/custom_exceptions.py @@ -129,7 +129,7 @@ def __str__(self) -> str: 'If you use an on-premises installation or a proxy that intercepts SSL traffic, ' f'set {consts.ENABLE_TRUSTSTORE_ENV_VAR_NAME}=1 to trust the CA certificates installed in your ' f'machine certificate store, or {_SSL_ERROR_CA_BUNDLE_HINT}' - if trust_store.is_supported() and not trust_store.is_enabled() + if not trust_store.is_enabled() else f'If you use an on-premises installation or a proxy that intercepts SSL traffic, {_SSL_ERROR_CA_BUNDLE_HINT}' ) diff --git a/cycode/cli/exceptions/handle_errors.py b/cycode/cli/exceptions/handle_errors.py index ded1d88c..895a9524 100644 --- a/cycode/cli/exceptions/handle_errors.py +++ b/cycode/cli/exceptions/handle_errors.py @@ -1,5 +1,3 @@ -from typing import Optional - import click import typer @@ -8,7 +6,7 @@ def handle_errors( ctx: typer.Context, err: BaseException, cli_errors: CliErrors, *, return_exception: bool = False -) -> Optional['CliError']: +) -> CliError | None: printer = ctx.obj.get('console_printer') printer.print_exception(err) diff --git a/cycode/cli/exceptions/handle_scan_errors.py b/cycode/cli/exceptions/handle_scan_errors.py index 5e2d9776..5107165e 100644 --- a/cycode/cli/exceptions/handle_scan_errors.py +++ b/cycode/cli/exceptions/handle_scan_errors.py @@ -1,5 +1,4 @@ import zipfile -from typing import Optional import typer @@ -10,7 +9,7 @@ from cycode.cli.utils.git_proxy import git_proxy -def handle_scan_exception(ctx: typer.Context, err: Exception, *, return_exception: bool = False) -> Optional[CliError]: +def handle_scan_exception(ctx: typer.Context, err: Exception, *, return_exception: bool = False) -> CliError | None: ctx.obj['did_fail'] = True errors: CliErrors = { diff --git a/cycode/cli/files_collector/commit_range_documents.py b/cycode/cli/files_collector/commit_range_documents.py index d5fdcc2f..3713f7e1 100644 --- a/cycode/cli/files_collector/commit_range_documents.py +++ b/cycode/cli/files_collector/commit_range_documents.py @@ -1,6 +1,6 @@ import os import sys -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING import typer @@ -47,7 +47,7 @@ def get_safe_head_reference_for_diff(repo: 'Repo') -> str: return consts.GIT_EMPTY_TREE_OBJECT -def get_staged_diff_index(repo: 'Repo', paths: Optional[list[str]] = None) -> tuple[str, 'DiffIndex']: +def get_staged_diff_index(repo: 'Repo', paths: list[str] | None = None) -> tuple[str, 'DiffIndex']: """Diff the index against HEAD, or against the empty tree in repositories with no commits. GitPython only inverts the `R` flag for HEAD, so `R` must be off for the empty tree to keep @@ -65,7 +65,7 @@ def get_staged_diff_index(repo: 'Repo', paths: Optional[list[str]] = None) -> tu return head_reference, repo.index.diff(head_reference, create_patch=True, R=reverse, paths=paths or None) -def _does_reach_to_max_commits_to_scan_limit(commit_ids: list[str], max_commits_count: Optional[int]) -> bool: +def _does_reach_to_max_commits_to_scan_limit(commit_ids: list[str], max_commits_count: int | None) -> bool: if max_commits_count is None: return False @@ -73,7 +73,7 @@ def _does_reach_to_max_commits_to_scan_limit(commit_ids: list[str], max_commits_ def collect_commit_range_diff_documents( - ctx: typer.Context, path: str, commit_range: str, max_commits_count: Optional[int] = None + ctx: typer.Context, path: str, commit_range: str, max_commits_count: int | None = None ) -> list[Document]: """Collects documents from a specified commit range in a Git repository. @@ -138,7 +138,7 @@ def collect_commit_range_diff_documents( return commit_documents_to_scan -def calculate_pre_receive_commit_range(repo_path: str, branch_update_details: str) -> Optional[str]: +def calculate_pre_receive_commit_range(repo_path: str, branch_update_details: str) -> str | None: end_commit = _get_end_commit_from_branch_update_details(branch_update_details) # branch is deleted, no need to perform scan @@ -168,7 +168,7 @@ def _get_end_commit_from_branch_update_details(update_details: str) -> str: return end_commit -def _get_oldest_unupdated_commit_for_branch(repo: 'Repo', commit: str) -> Optional[str]: +def _get_oldest_unupdated_commit_for_branch(repo: 'Repo', commit: str) -> str | None: # get a list of commits by chronological order that are not in the remote repository yet # more info about rev-list command: https://git-scm.com/docs/git-rev-list @@ -181,12 +181,12 @@ def _get_oldest_unupdated_commit_for_branch(repo: 'Repo', commit: str) -> Option return commits[0] -def _get_file_content_from_commit_diff(repo: 'Repo', commit: str, diff: 'Diff') -> Optional[str]: +def _get_file_content_from_commit_diff(repo: 'Repo', commit: str, diff: 'Diff') -> str | None: file_path = get_diff_file_path(diff, relative=True) return get_file_content_from_commit_path(repo, commit, file_path) -def _get_blob_content(blob: Optional['Blob']) -> Optional[str]: +def _get_blob_content(blob: 'Blob | None') -> str | None: """Read a diff-side blob's content in-process, with no subprocess spawn. `diff.a_blob`/`diff.b_blob` already hold whatever git loaded to compute the patch, so this is @@ -266,7 +266,7 @@ def parse_pre_receive_input() -> str: return pre_receive_input.splitlines()[0] -def parse_pre_push_input() -> Optional[str]: +def parse_pre_push_input() -> str | None: """Parse input to pre-push hook details. Example input: @@ -289,7 +289,7 @@ def is_invoked_by_pre_commit_framework() -> bool: return os.getenv(consts.PRE_COMMIT_FRAMEWORK_ENV_VAR_NAME) == '1' -def get_pre_commit_framework_push_range() -> Optional[str]: +def get_pre_commit_framework_push_range() -> str | None: """Get the commit range to scan when invoked as a pre-push hook by the pre-commit framework. The pre-commit framework reads git's pre-push stdin itself and never forwards it to hooks. @@ -379,7 +379,7 @@ def _get_default_branches_for_merge_base(repo: 'Repo') -> list[str]: return default_branches -def calculate_pre_push_commit_range(push_update_details: str) -> Optional[str]: +def calculate_pre_push_commit_range(push_update_details: str) -> str | None: """Calculate the commit range for pre-push hook scanning. Args: @@ -430,7 +430,7 @@ def calculate_pre_push_commit_range(push_update_details: str) -> Optional[str]: return f'{remote_object_name}..{local_object_name}' -def get_diff_file_path(diff: 'Diff', relative: bool = False, repo: Optional['Repo'] = None) -> Optional[str]: +def get_diff_file_path(diff: 'Diff', relative: bool = False, repo: 'Repo | None' = None) -> str | None: """Get the file path from a git Diff object. Args: @@ -470,7 +470,7 @@ def get_pre_commit_modified_documents( repo_path: str, base_ref: str = consts.GIT_HEAD_COMMIT_REV, include_unstaged: bool = False, - paths: Optional[list[str]] = None, + paths: list[str] | None = None, collect_file_contents: bool = True, ) -> tuple[list[Document], list[Document], list[Document]]: """Diffs `base_ref` against the staged index (default) or the full working tree. @@ -562,7 +562,7 @@ def get_pre_commit_modified_documents( return from_ref_documents, working_copy_documents, diff_documents -def parse_commit_range(commit_range: str, path: str) -> tuple[Optional[str], Optional[str], Optional[str]]: +def parse_commit_range(commit_range: str, path: str) -> tuple[str | None, str | None, str | None]: """Parses a git commit range string and returns the full SHAs for the 'from' and 'to' commits. Also, it returns the separator in the commit range. diff --git a/cycode/cli/files_collector/models/in_memory_zip.py b/cycode/cli/files_collector/models/in_memory_zip.py index 5da13113..b83e4833 100644 --- a/cycode/cli/files_collector/models/in_memory_zip.py +++ b/cycode/cli/files_collector/models/in_memory_zip.py @@ -3,7 +3,7 @@ from collections import defaultdict from os import SEEK_END from pathlib import Path -from typing import IO, Optional +from typing import IO from zipfile import ZIP_DEFLATED, ZipFile from cycode.cli import consts @@ -17,7 +17,7 @@ _SPOOL_DIRECTORY_NAME = 'tmp' -def _get_spool_directory(configuration_manager: ConfigurationManager) -> Optional[str]: +def _get_spool_directory(configuration_manager: ConfigurationManager) -> str | None: """Directory to spill big ZIPs into. None falls back to the system temp directory.""" try: directory = Path(configuration_manager.global_config_file_manager.get_config_directory_path()) @@ -47,7 +47,7 @@ def __init__(self) -> None: self._files_count = 0 self._extension_statistics = defaultdict(int) - def append(self, filename: str, unique_id: Optional[str], content: str) -> None: + def append(self, filename: str, unique_id: str | None, content: str) -> None: self._files_count += 1 self._extension_statistics[Path(filename).suffix] += 1 diff --git a/cycode/cli/files_collector/repository_documents.py b/cycode/cli/files_collector/repository_documents.py index 935d3db1..ab9555e2 100644 --- a/cycode/cli/files_collector/repository_documents.py +++ b/cycode/cli/files_collector/repository_documents.py @@ -1,5 +1,5 @@ from collections.abc import Iterator -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING from cycode.cli.utils.git_proxy import git_proxy @@ -15,11 +15,11 @@ def _should_process_git_object(obj: 'Blob', _: int) -> bool: def get_git_repository_tree_file_entries( path: str, branch: str -) -> Union[Iterator['IndexObjUnion'], Iterator['TraversedTreeTup']]: +) -> Iterator['IndexObjUnion'] | Iterator['TraversedTreeTup']: return git_proxy.get_repo(path).tree(branch).traverse(predicate=_should_process_git_object) -def get_file_content_from_commit_path(repo: 'Repo', commit: str, file_path: str) -> Optional[str]: +def get_file_content_from_commit_path(repo: 'Repo', commit: str, file_path: str) -> str | None: try: return repo.git.show(f'{commit}:{file_path}') except git_proxy.get_git_command_error(): diff --git a/cycode/cli/files_collector/sca/base_restore_dependencies.py b/cycode/cli/files_collector/sca/base_restore_dependencies.py index d5167e92..b5397d40 100644 --- a/cycode/cli/files_collector/sca/base_restore_dependencies.py +++ b/cycode/cli/files_collector/sca/base_restore_dependencies.py @@ -1,6 +1,5 @@ from abc import ABC, abstractmethod from pathlib import Path -from typing import Optional import typer @@ -19,9 +18,9 @@ def build_dep_tree_path(path: str, generated_file_name: str) -> str: def execute_commands( commands: list[list[str]], timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, -) -> Optional[str]: + output_file_path: str | None = None, + working_directory: str | None = None, +) -> str | None: logger.debug( 'Executing restore commands, %s', { @@ -67,7 +66,7 @@ def __init__( self.command_timeout = command_timeout self.create_output_file_manually = create_output_file_manually - def restore(self, document: Document) -> Optional[Document]: + def restore(self, document: Document) -> Document | None: return self.try_restore_dependencies(document) def get_manifest_file_path(self, document: Document) -> str: @@ -75,7 +74,7 @@ def get_manifest_file_path(self, document: Document) -> str: join_paths(get_path_from_context(self.ctx), document.path) if self.ctx.obj.get('monitor') else document.path ) - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_file_path = self.get_manifest_file_path(document) restore_file_paths = [ build_dep_tree_path(document.absolute_path, restore_file_path_item) @@ -122,7 +121,7 @@ def try_restore_dependencies(self, document: Document) -> Optional[Document]: return Document(relative_restore_file_path, restore_file_content, self.is_git_diff) - def get_manifest_dir(self, document: Document) -> Optional[str]: + def get_manifest_dir(self, document: Document) -> str | None: """Return the directory containing the manifest file, resolving monitor-mode paths. Uses the same path resolution as get_manifest_file_path() to ensure consistency. @@ -143,7 +142,7 @@ def get_manifest_dir(self, document: Document) -> Optional[str]: return None - def get_working_directory(self, document: Document) -> Optional[str]: + def get_working_directory(self, document: Document) -> str | None: return str(Path(document.absolute_path).parent) def get_restored_lock_file_name(self, restore_file_path: str) -> str: diff --git a/cycode/cli/files_collector/sca/go/restore_go_dependencies.py b/cycode/cli/files_collector/sca/go/restore_go_dependencies.py index b98fbaf5..5a0d641a 100644 --- a/cycode/cli/files_collector/sca/go/restore_go_dependencies.py +++ b/cycode/cli/files_collector/sca/go/restore_go_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional import typer @@ -19,7 +18,7 @@ class RestoreGoDependencies(BaseRestoreDependencies): def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) -> None: super().__init__(ctx, is_git_diff, command_timeout, create_output_file_manually=True) - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_exists = (Path(self.get_working_directory(document)) / BUILD_GO_FILE_NAME).is_file() lock_exists = (Path(self.get_working_directory(document)) / BUILD_GO_LOCK_FILE_NAME).is_file() diff --git a/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py b/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py index ea91a8de..4ffd3e96 100644 --- a/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +++ b/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py @@ -1,7 +1,6 @@ import os import platform import re -from typing import Optional import typer @@ -22,7 +21,7 @@ class RestoreGradleDependencies(BaseRestoreDependencies): def __init__( - self, ctx: typer.Context, is_git_diff: bool, command_timeout: int, projects: Optional[set[str]] = None + self, ctx: typer.Context, is_git_diff: bool, command_timeout: int, projects: set[str] | None = None ) -> None: super().__init__(ctx, is_git_diff, command_timeout, create_output_file_manually=True) self.gradle_executable = self._resolve_gradle_executable() @@ -58,7 +57,7 @@ def get_lock_file_name(self) -> str: def get_lock_file_names(self) -> list[str]: return [self.get_lock_file_name()] - def get_working_directory(self, document: Document) -> Optional[str]: + def get_working_directory(self, document: Document) -> str | None: return get_path_from_context(self.ctx) if self.is_gradle_sub_projects() else None def get_all_projects(self) -> set[str]: diff --git a/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py b/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py index 53ed269f..75f61545 100644 --- a/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +++ b/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py @@ -1,7 +1,6 @@ import json from os import path from pathlib import Path -from typing import Optional import typer @@ -21,7 +20,7 @@ MAVEN_DEP_TREE_FILE_NAME = 'bcde.mvndeps' -def _has_dependency_graph(bom_content: Optional[str]) -> bool: +def _has_dependency_graph(bom_content: str | None) -> bool: try: if not bom_content: return False @@ -52,7 +51,7 @@ def get_lock_file_name(self) -> str: def get_lock_file_names(self) -> list[str]: return [self.get_lock_file_name()] - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_file_path = self.get_manifest_file_path(document) if document.content is None: return self.restore_from_secondary_command(document, manifest_file_path) @@ -68,7 +67,7 @@ def try_restore_dependencies(self, document: Document) -> Optional[Document]: return restore_dependencies_document - def restore_from_secondary_command(self, document: Document, manifest_file_path: str) -> Optional[Document]: + def restore_from_secondary_command(self, document: Document, manifest_file_path: str) -> Document | None: restore_content = execute_commands( commands=self.create_secondary_restore_commands(manifest_file_path), timeout=self.command_timeout, diff --git a/cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py b/cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py index 72c65c21..7174eddc 100644 --- a/cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py +++ b/cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py @@ -1,7 +1,6 @@ import json import re from pathlib import Path -from typing import Optional import typer @@ -26,7 +25,7 @@ BUN_VERSION_COMMAND = ['bun', '--version'] -def _indicates_bun(package_json_content: Optional[str]) -> bool: +def _indicates_bun(package_json_content: str | None) -> bool: """Return True if package.json content signals that this project uses Bun.""" if not package_json_content: return False @@ -43,7 +42,7 @@ def _indicates_bun(package_json_content: Optional[str]) -> bool: return isinstance(engines, dict) and 'bun' in engines -def _parse_bun_version(raw_version: Optional[str]) -> Optional[tuple[int, int]]: +def _parse_bun_version(raw_version: str | None) -> tuple[int, int] | None: """Parse the (major, minor) version from `bun --version` output (e.g. '1.2.3').""" if not raw_version: return None @@ -91,7 +90,7 @@ def _is_supported_bun_version(self) -> bool: return False return True - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_dir = self.get_manifest_dir(document) lockfile_path = Path(manifest_dir) / BUN_LOCK_FILE_NAME if manifest_dir else None diff --git a/cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py b/cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py index d81b7a9d..713c4f06 100644 --- a/cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +++ b/cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional import typer @@ -21,7 +20,7 @@ def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) def is_project(self, document: Document) -> bool: return Path(document.path).name in DENO_MANIFEST_FILE_NAMES - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_dir = self.get_manifest_dir(document) if not manifest_dir: return None diff --git a/cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py b/cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py index 159ac506..95114904 100644 --- a/cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +++ b/cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py @@ -1,6 +1,5 @@ import json from pathlib import Path -from typing import Optional import typer @@ -19,7 +18,7 @@ PNPM_MANIFEST_FILE_NAME = MANIFEST_FILE_NAME -def _indicates_pnpm(package_json_content: Optional[str]) -> bool: +def _indicates_pnpm(package_json_content: str | None) -> bool: """Return True if package.json content signals that this project uses pnpm.""" if not package_json_content: return False @@ -53,7 +52,7 @@ def is_project(self, document: Document) -> bool: return _indicates_pnpm(document.content) - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_dir = self.get_manifest_dir(document) lockfile_path = Path(manifest_dir) / PNPM_LOCK_FILE_NAME if manifest_dir else None diff --git a/cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py b/cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py index 9b4cbb11..3ad1bc3e 100644 --- a/cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +++ b/cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py @@ -1,6 +1,5 @@ import json from pathlib import Path -from typing import Optional import typer @@ -19,7 +18,7 @@ YARN_MANIFEST_FILE_NAME = MANIFEST_FILE_NAME -def _indicates_yarn(package_json_content: Optional[str]) -> bool: +def _indicates_yarn(package_json_content: str | None) -> bool: """Return True if package.json content signals that this project uses Yarn.""" if not package_json_content: return False @@ -53,7 +52,7 @@ def is_project(self, document: Document) -> bool: return _indicates_yarn(document.content) - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_dir = self.get_manifest_dir(document) lockfile_path = Path(manifest_dir) / YARN_LOCK_FILE_NAME if manifest_dir else None diff --git a/cycode/cli/files_collector/sca/npm/workspace/coverage.py b/cycode/cli/files_collector/sca/npm/workspace/coverage.py index 7512cb60..952a7c76 100644 --- a/cycode/cli/files_collector/sca/npm/workspace/coverage.py +++ b/cycode/cli/files_collector/sca/npm/workspace/coverage.py @@ -2,7 +2,7 @@ import os from pathlib import Path -from typing import TYPE_CHECKING, NamedTuple, Optional +from typing import TYPE_CHECKING, NamedTuple from cycode.cli.files_collector.sca.npm.workspace.files import logger, resolved_path from cycode.cli.files_collector.sca.npm.workspace.globs import declares_workspace_member @@ -49,7 +49,7 @@ def _containing_scan_roots(manifest_dir: Path, scan_roots: tuple) -> list: ] -def _resolve_walk_boundary(manifest_dir: Path, scan_roots: tuple) -> Optional[Path]: +def _resolve_walk_boundary(manifest_dir: Path, scan_roots: tuple) -> Path | None: for root_dir in manifest_dir.parents: if (root_dir / _GIT_DIR_NAME).exists(): return root_dir @@ -76,7 +76,7 @@ def _workspace_root_candidates(manifest_dir: Path, scan_roots: tuple) -> 'Iterat return -def _find_covering_workspace(manifest_dir: Path, scan_roots: tuple) -> Optional[WorkspaceCoverage]: +def _find_covering_workspace(manifest_dir: Path, scan_roots: tuple) -> WorkspaceCoverage | None: for root_dir in _workspace_root_candidates(manifest_dir, scan_roots): member_path = manifest_dir.relative_to(root_dir).as_posix() @@ -98,7 +98,7 @@ def _find_covering_workspace(manifest_dir: Path, scan_roots: tuple) -> Optional[ return None -def find_covering_workspace(manifest_dir: Optional[str], scan_roots: tuple = ()) -> Optional[WorkspaceCoverage]: +def find_covering_workspace(manifest_dir: str | None, scan_roots: tuple = ()) -> WorkspaceCoverage | None: if not manifest_dir: return None @@ -115,7 +115,7 @@ def _is_inside_scanned_paths(scan_roots: tuple, root_dir: Path) -> bool: return any(is_sub_path(directory, resolved_root_dir) for directory in directories) -def is_covered_workspace_member(manifest_dir: Optional[str], document_path: str, scan_roots: tuple = ()) -> bool: +def is_covered_workspace_member(manifest_dir: str | None, document_path: str, scan_roots: tuple = ()) -> bool: coverage = find_covering_workspace(manifest_dir, scan_roots) if coverage is None: return False diff --git a/cycode/cli/files_collector/sca/npm/workspace/files.py b/cycode/cli/files_collector/sca/npm/workspace/files.py index e51485da..16b3e687 100644 --- a/cycode/cli/files_collector/sca/npm/workspace/files.py +++ b/cycode/cli/files_collector/sca/npm/workspace/files.py @@ -3,7 +3,6 @@ import json import os from pathlib import Path -from typing import Optional from cycode.cli.utils.path_utils import get_absolute_path from cycode.logger import get_logger @@ -17,7 +16,7 @@ def resolved_path(path: object) -> str: return os.path.realpath(get_absolute_path(str(path))) -def file_stamp(path: Path) -> Optional[FileStamp]: +def file_stamp(path: Path) -> FileStamp | None: try: stat_result = path.stat() except OSError: @@ -26,7 +25,7 @@ def file_stamp(path: Path) -> Optional[FileStamp]: return str(path), stat_result.st_mtime_ns, stat_result.st_size -def read_json_object(path: Path) -> Optional[dict]: +def read_json_object(path: Path) -> dict | None: try: content = json.loads(path.read_text(encoding='UTF-8')) except FileNotFoundError: diff --git a/cycode/cli/files_collector/sca/npm/workspace/globs.py b/cycode/cli/files_collector/sca/npm/workspace/globs.py index 03dcd559..e97a0e15 100644 --- a/cycode/cli/files_collector/sca/npm/workspace/globs.py +++ b/cycode/cli/files_collector/sca/npm/workspace/globs.py @@ -72,8 +72,7 @@ def _split_workspace_patterns(declared: list) -> _WorkspacePatterns: stripped = entry.strip() is_excluded = stripped.startswith(_NEGATION_PREFIX) normalized = (stripped[1:] if is_excluded else stripped).strip() - if normalized.startswith('./'): - normalized = normalized[2:] + normalized = normalized.removeprefix('./') normalized = normalized.rstrip('/') if not normalized: diff --git a/cycode/cli/files_collector/sca/npm/workspace/resolvers.py b/cycode/cli/files_collector/sca/npm/workspace/resolvers.py index 26ef1e83..175efd29 100644 --- a/cycode/cli/files_collector/sca/npm/workspace/resolvers.py +++ b/cycode/cli/files_collector/sca/npm/workspace/resolvers.py @@ -9,7 +9,6 @@ import re from abc import ABC, abstractmethod from pathlib import Path -from typing import Optional import yaml @@ -38,17 +37,16 @@ _BUN_LOCKFILE_WORKSPACES_SECTION = 'workspaces' _TRAILING_COMMA = re.compile(r',(\s*[}\]])') -_member_names_cache: dict[FileStamp, Optional[frozenset[str]]] = {} +_member_names_cache: dict[FileStamp, frozenset[str] | None] = {} def clear_cache() -> None: _member_names_cache.clear() -def _normalize_member_path(member_path: str) -> Optional[str]: +def _normalize_member_path(member_path: str) -> str | None: normalized = member_path.strip() - if normalized.startswith('./'): - normalized = normalized[2:] + normalized = normalized.removeprefix('./') normalized = normalized.rstrip('/') if not normalized or normalized == _PNPM_LOCKFILE_ROOT_IMPORTER: @@ -57,7 +55,7 @@ def _normalize_member_path(member_path: str) -> Optional[str]: return normalized -def _npm_lockfile_member_names(lock_file: Path) -> Optional[frozenset]: +def _npm_lockfile_member_names(lock_file: Path) -> frozenset | None: content = read_json_object(lock_file) packages = content.get(_LOCKFILE_PACKAGES_SECTION) if content is not None else None if not isinstance(packages, dict): @@ -96,7 +94,7 @@ def _read_pnpm_importers_section(lock_file: Path) -> str: return '\n'.join(section) -def _pnpm_lockfile_member_names(lock_file: Path) -> Optional[frozenset]: +def _pnpm_lockfile_member_names(lock_file: Path) -> frozenset | None: section = _read_pnpm_importers_section(lock_file) if not section: return None @@ -124,7 +122,7 @@ def _pnpm_lockfile_member_names(lock_file: Path) -> Optional[frozenset]: return frozenset(member_names) or None -def _bun_lockfile_member_names(lock_file: Path) -> Optional[frozenset]: +def _bun_lockfile_member_names(lock_file: Path) -> frozenset | None: """bun.lock is JSON with trailing commas, and names its members under "workspaces".""" try: text = lock_file.read_text(encoding='UTF-8') @@ -151,7 +149,7 @@ def _bun_lockfile_member_names(lock_file: Path) -> Optional[frozenset]: return frozenset(member_names) -def _yarn_lockfile_member_names(lock_file: Path) -> Optional[frozenset]: +def _yarn_lockfile_member_names(lock_file: Path) -> frozenset | None: """Yarn berry records every member as "@workspace:"; classic yarn records nothing.""" try: text = lock_file.read_text(encoding='UTF-8', errors='replace') @@ -201,7 +199,7 @@ def may_use_workspace_globs(self) -> bool: """ return False - def resolve(self, lock_file: Path) -> Optional[frozenset]: + def resolve(self, lock_file: Path) -> frozenset | None: """Member paths this lockfile resolves, or None when it cannot name them. Caches on the file's identity so one scan parses each root lockfile once. @@ -218,14 +216,14 @@ def resolve(self, lock_file: Path) -> Optional[frozenset]: return member_names @abstractmethod - def _read_member_names(self, lock_file: Path) -> Optional[frozenset]: ... + def _read_member_names(self, lock_file: Path) -> frozenset | None: ... class NpmLockfileResolver(WorkspaceMemberResolver): package_manager = NPM_PACKAGE_MANAGER lock_file_names = (NPM_LOCK_FILE_NAME, NPM_SHRINKWRAP_FILE_NAME) - def _read_member_names(self, lock_file: Path) -> Optional[frozenset]: + def _read_member_names(self, lock_file: Path) -> frozenset | None: return _npm_lockfile_member_names(lock_file) @@ -233,7 +231,7 @@ class PnpmLockfileResolver(WorkspaceMemberResolver): package_manager = PNPM_PACKAGE_MANAGER lock_file_names = (PNPM_LOCK_FILE_NAME,) - def _read_member_names(self, lock_file: Path) -> Optional[frozenset]: + def _read_member_names(self, lock_file: Path) -> frozenset | None: return _pnpm_lockfile_member_names(lock_file) @@ -244,7 +242,7 @@ class YarnLockfileResolver(WorkspaceMemberResolver): lock_file_names = (YARN_LOCK_FILE_NAME,) may_use_workspace_globs = True - def _read_member_names(self, lock_file: Path) -> Optional[frozenset]: + def _read_member_names(self, lock_file: Path) -> frozenset | None: return _yarn_lockfile_member_names(lock_file) @@ -254,7 +252,7 @@ class BunLockfileResolver(WorkspaceMemberResolver): package_manager = BUN_PACKAGE_MANAGER lock_file_names = (BUN_LOCK_FILE_NAME,) - def _read_member_names(self, lock_file: Path) -> Optional[frozenset]: + def _read_member_names(self, lock_file: Path) -> frozenset | None: return _bun_lockfile_member_names(lock_file) @@ -275,7 +273,7 @@ def package_manager(self) -> str: def lock_file_names(self) -> tuple: return self._lock_file_names - def _read_member_names(self, lock_file: Path) -> Optional[frozenset]: + def _read_member_names(self, lock_file: Path) -> frozenset | None: return None diff --git a/cycode/cli/files_collector/sca/php/restore_composer_dependencies.py b/cycode/cli/files_collector/sca/php/restore_composer_dependencies.py index 98b3564c..d6669493 100644 --- a/cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +++ b/cycode/cli/files_collector/sca/php/restore_composer_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional import typer @@ -21,7 +20,7 @@ def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) def is_project(self, document: Document) -> bool: return Path(document.path).name == COMPOSER_MANIFEST_FILE_NAME - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_dir = self.get_manifest_dir(document) lockfile_path = Path(manifest_dir) / COMPOSER_LOCK_FILE_NAME if manifest_dir else None diff --git a/cycode/cli/files_collector/sca/python/restore_pip_dependencies.py b/cycode/cli/files_collector/sca/python/restore_pip_dependencies.py index 29ebfc6e..60423ddd 100644 --- a/cycode/cli/files_collector/sca/python/restore_pip_dependencies.py +++ b/cycode/cli/files_collector/sca/python/restore_pip_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional import typer @@ -18,7 +17,7 @@ _UV_TOOL_SECTION = '[tool.uv]' -def _indicates_plain_pip(pyproject_content: Optional[str]) -> bool: +def _indicates_plain_pip(pyproject_content: str | None) -> bool: """Return True if pyproject.toml content signals a plain-pip project (no Poetry, no uv).""" if not pyproject_content: return False @@ -44,7 +43,7 @@ def is_project(self, document: Document) -> bool: return _indicates_plain_pip(document.content) - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_dir = self.get_manifest_dir(document) lockfile_path = Path(manifest_dir) / PIP_LOCK_FILE_NAME if manifest_dir else None diff --git a/cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py b/cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py index df91707c..458910d0 100644 --- a/cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +++ b/cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional import typer @@ -21,7 +20,7 @@ def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) def is_project(self, document: Document) -> bool: return Path(document.path).name == PIPENV_MANIFEST_FILE_NAME - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_dir = self.get_manifest_dir(document) lockfile_path = Path(manifest_dir) / PIPENV_LOCK_FILE_NAME if manifest_dir else None diff --git a/cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py b/cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py index f681bd63..1ace0d92 100644 --- a/cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +++ b/cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional import typer @@ -17,7 +16,7 @@ _POETRY_TOOL_SECTION = '[tool.poetry]' -def _indicates_poetry(pyproject_content: Optional[str]) -> bool: +def _indicates_poetry(pyproject_content: str | None) -> bool: """Return True if pyproject.toml content signals that this project uses Poetry.""" if not pyproject_content: return False @@ -38,7 +37,7 @@ def is_project(self, document: Document) -> bool: return _indicates_poetry(document.content) - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_dir = self.get_manifest_dir(document) lockfile_path = Path(manifest_dir) / POETRY_LOCK_FILE_NAME if manifest_dir else None diff --git a/cycode/cli/files_collector/sca/python/restore_uv_dependencies.py b/cycode/cli/files_collector/sca/python/restore_uv_dependencies.py index c05d857c..c82d1183 100644 --- a/cycode/cli/files_collector/sca/python/restore_uv_dependencies.py +++ b/cycode/cli/files_collector/sca/python/restore_uv_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional import typer @@ -16,7 +15,7 @@ _UV_TOOL_SECTION = '[tool.uv]' -def _indicates_uv(pyproject_content: Optional[str]) -> bool: +def _indicates_uv(pyproject_content: str | None) -> bool: """Return True if pyproject.toml content signals that this project uses UV.""" if not pyproject_content: return False @@ -37,7 +36,7 @@ def is_project(self, document: Document) -> bool: return _indicates_uv(document.content) - def try_restore_dependencies(self, document: Document) -> Optional[Document]: + def try_restore_dependencies(self, document: Document) -> Document | None: manifest_dir = self.get_manifest_dir(document) lockfile_path = Path(manifest_dir) / UV_LOCK_FILE_NAME if manifest_dir else None diff --git a/cycode/cli/files_collector/sca/sca_file_collector.py b/cycode/cli/files_collector/sca/sca_file_collector.py index 10b1687b..55602911 100644 --- a/cycode/cli/files_collector/sca/sca_file_collector.py +++ b/cycode/cli/files_collector/sca/sca_file_collector.py @@ -1,5 +1,5 @@ import os -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING import typer @@ -39,7 +39,7 @@ def _add_ecosystem_related_files_if_exists( - documents: list[Document], repo: Optional['Repo'] = None, commit_rev: Optional[str] = None + documents: list[Document], repo: 'Repo | None' = None, commit_rev: str | None = None ) -> None: documents_to_add: list[Document] = [] for doc in documents: @@ -81,7 +81,7 @@ def perform_sca_pre_commit_scan_actions( def _get_doc_ecosystem_related_project_files( - doc: Document, documents: list[Document], ecosystem: str, commit_rev: Optional[str], repo: Optional['Repo'] + doc: Document, documents: list[Document], ecosystem: str, commit_rev: str | None, repo: 'Repo | None' ) -> list[Document]: documents_to_add: list[Document] = [] for ecosystem_project_file in consts.PROJECT_FILES_BY_ECOSYSTEM_MAP.get(ecosystem): @@ -102,7 +102,7 @@ def _is_project_file_exists_in_documents(documents: list[Document], file: str) - return any(doc for doc in documents if file == doc.path) -def _get_project_file_ecosystem(document: Document) -> Optional[str]: +def _get_project_file_ecosystem(document: Document) -> str | None: for ecosystem, project_files in consts.PROJECT_FILES_BY_ECOSYSTEM_MAP.items(): for project_file in project_files: if document.path.endswith(project_file): @@ -118,7 +118,7 @@ def _try_restore_dependencies( ctx: typer.Context, restore_dependencies: 'BaseRestoreDependencies', document: Document, -) -> Optional[Document]: +) -> Document | None: if not restore_dependencies.is_project(document): return None diff --git a/cycode/cli/files_collector/zip_documents.py b/cycode/cli/files_collector/zip_documents.py index 7927bdc6..a85d0e4e 100644 --- a/cycode/cli/files_collector/zip_documents.py +++ b/cycode/cli/files_collector/zip_documents.py @@ -1,6 +1,5 @@ import timeit from pathlib import Path -from typing import Optional from cycode.cli import consts from cycode.cli.exceptions import custom_exceptions @@ -20,7 +19,7 @@ def _validate_zip_file_size(scan_type: str, zip_file_size: int) -> None: def zip_documents( scan_type: str, documents: list[Document], - zip_file: Optional[InMemoryZip] = None, + zip_file: InMemoryZip | None = None, ) -> InMemoryZip: if zip_file is None: zip_file = InMemoryZip() diff --git a/cycode/cli/models.py b/cycode/cli/models.py index ada31137..c797aedf 100644 --- a/cycode/cli/models.py +++ b/cycode/cli/models.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import NamedTuple, Optional +from typing import NamedTuple from cycode.cyclient.models import Detection @@ -10,8 +10,8 @@ def __init__( path: str, content: str, is_git_diff_format: bool = False, - unique_id: Optional[str] = None, - absolute_path: Optional[str] = None, + unique_id: str | None = None, + absolute_path: str | None = None, ) -> None: self.path = path self.content = content @@ -48,25 +48,25 @@ def enrich(self, additional_message: str) -> 'CliError': class CliResult(NamedTuple): success: bool message: str - data: Optional[dict[str, any]] = None + data: dict[str, any] | None = None class LocalScanResult(NamedTuple): scan_id: str - report_url: Optional[str] + report_url: str | None document_detections: list[DocumentDetections] issue_detected: bool detections_count: int relevant_detections_count: int - verdict: Optional[str] = None + verdict: str | None = None @dataclass class ResourceChange: - module_address: Optional[str] + module_address: str | None resource_type: str name: str - index: Optional[int] + index: int | None actions: list[str] values: dict[str, str] diff --git a/cycode/cli/printers/console_printer.py b/cycode/cli/printers/console_printer.py index 50d48fd7..d3617384 100644 --- a/cycode/cli/printers/console_printer.py +++ b/cycode/cli/printers/console_printer.py @@ -1,5 +1,5 @@ import io -from typing import TYPE_CHECKING, ClassVar, Optional +from typing import TYPE_CHECKING, ClassVar import typer from rich.console import Console @@ -35,18 +35,18 @@ class ConsolePrinter: def __init__( self, ctx: typer.Context, - console_override: Optional['Console'] = None, - console_err_override: Optional['Console'] = None, - output_type_override: Optional[str] = None, + console_override: 'Console | None' = None, + console_err_override: 'Console | None' = None, + output_type_override: str | None = None, ) -> None: self.ctx = ctx self.console = console_override or console self.console_err = console_err_override or console_err self.output_type = output_type_override or self.ctx.obj.get('output') - self.export_type: Optional[str] = None - self.export_file: Optional[Path] = None - self.console_record: Optional[ConsolePrinter] = None + self.export_type: str | None = None + self.export_file: Path | None = None + self.console_record: ConsolePrinter | None = None @property def scan_type(self) -> str: @@ -87,7 +87,7 @@ def enable_recording(self, export_type: str, export_file: 'Path') -> None: def print_scan_results( self, local_scan_results: list['LocalScanResult'], - errors: Optional[dict[str, 'CliError']] = None, + errors: dict[str, 'CliError'] | None = None, ) -> None: if self.console_record: self.console_record.print_scan_results(local_scan_results, errors) @@ -103,7 +103,7 @@ def print_error(self, error: CliError) -> None: self.console_record.print_error(error) self.printer.print_error(error) - def print_exception(self, e: Optional[BaseException] = None, force_print: bool = False) -> None: + def print_exception(self, e: BaseException | None = None, force_print: bool = False) -> None: """Print traceback message in stderr if verbose mode is set.""" if force_print or self.ctx.obj.get('verbose', False): if self.console_record: diff --git a/cycode/cli/printers/json_printer.py b/cycode/cli/printers/json_printer.py index acb7912f..fcf23e40 100644 --- a/cycode/cli/printers/json_printer.py +++ b/cycode/cli/printers/json_printer.py @@ -1,5 +1,5 @@ import json -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from cycode.cli.models import CliError, CliResult from cycode.cli.printers.printer_base import PrinterBase @@ -21,7 +21,7 @@ def print_error(self, error: CliError) -> None: self.console.print_json(self.get_data_json(result)) def print_scan_results( - self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: dict[str, 'CliError'] | None = None ) -> None: scan_ids = [] report_urls = [] diff --git a/cycode/cli/printers/printer_base.py b/cycode/cli/printers/printer_base.py index 69596e2a..e896cc6c 100644 --- a/cycode/cli/printers/printer_base.py +++ b/cycode/cli/printers/printer_base.py @@ -1,7 +1,7 @@ import sys from abc import ABC, abstractmethod from collections import defaultdict -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING import typer @@ -51,7 +51,7 @@ def show_secret(self) -> bool: @abstractmethod def print_scan_results( - self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: dict[str, 'CliError'] | None = None ) -> None: pass @@ -63,7 +63,7 @@ def print_result(self, result: CliResult) -> None: def print_error(self, error: CliError) -> None: pass - def print_exception(self, e: Optional[BaseException] = None) -> None: + def print_exception(self, e: BaseException | None = None) -> None: """We are printing it in stderr so, we don't care about supporting JSON and TABLE outputs. Note: diff --git a/cycode/cli/printers/rich_printer.py b/cycode/cli/printers/rich_printer.py index f326f166..6a2fd847 100644 --- a/cycode/cli/printers/rich_printer.py +++ b/cycode/cli/printers/rich_printer.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from rich.console import Group from rich.panel import Panel @@ -26,7 +26,7 @@ class RichPrinter(TextPrinter): MAX_PATH_LENGTH = 60 def print_scan_results( - self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: dict[str, 'CliError'] | None = None ) -> None: if not errors and all(result.issue_detected == 0 for result in local_scan_results): self.console.print(self.NO_DETECTIONS_MESSAGE) diff --git a/cycode/cli/printers/tables/table.py b/cycode/cli/printers/tables/table.py index 61e143ca..a562df6a 100644 --- a/cycode/cli/printers/tables/table.py +++ b/cycode/cli/printers/tables/table.py @@ -1,5 +1,5 @@ import urllib.parse -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from rich.markup import escape from rich.table import Table as RichTable @@ -11,7 +11,7 @@ class Table: """Helper class to manage columns and their values in the right order and only if the column should be presented.""" - def __init__(self, column_infos: Optional[list['ColumnInfo']] = None) -> None: + def __init__(self, column_infos: list['ColumnInfo'] | None = None) -> None: self._group_separator_indexes: set[int] = set() self._columns: dict[ColumnInfo, list[str]] = {} @@ -26,7 +26,7 @@ def _add_cell_no_error(self, column: 'ColumnInfo', value: str) -> None: if column in self._columns: self._columns[column].append(value) - def add_cell(self, column: 'ColumnInfo', value: str, color: Optional[str] = None) -> None: + def add_cell(self, column: 'ColumnInfo', value: str, color: str | None = None) -> None: if color: value = f'[{color}]{value}[/]' @@ -49,7 +49,7 @@ def get_columns_info(self) -> list['ColumnInfo']: def get_rows(self) -> list[str]: column_values = [self._columns[column_info] for column_info in self._get_ordered_columns()] - return list(zip(*column_values)) + return list(zip(*column_values, strict=False)) def get_table(self) -> 'RichTable': table = RichTable(expand=True, highlight=True) diff --git a/cycode/cli/printers/tables/table_models.py b/cycode/cli/printers/tables/table_models.py index 58e41aaa..58228e57 100644 --- a/cycode/cli/printers/tables/table_models.py +++ b/cycode/cli/printers/tables/table_models.py @@ -1,4 +1,4 @@ -from typing import NamedTuple, Optional +from typing import NamedTuple class ColumnInfoBuilder: @@ -14,7 +14,7 @@ def build(self, name: str, **column_opts) -> 'ColumnInfo': class ColumnInfo(NamedTuple): name: str index: int # Represents the order of the columns, starting from the left - column_opts: Optional[dict] = None + column_opts: dict | None = None def __hash__(self) -> int: return hash((self.name, self.index)) diff --git a/cycode/cli/printers/tables/table_printer_base.py b/cycode/cli/printers/tables/table_printer_base.py index 8cb4cbda..40e0d322 100644 --- a/cycode/cli/printers/tables/table_printer_base.py +++ b/cycode/cli/printers/tables/table_printer_base.py @@ -1,5 +1,5 @@ import abc -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from cycode.cli.models import CliError, CliResult from cycode.cli.printers.printer_base import PrinterBase @@ -22,7 +22,7 @@ def print_error(self, error: CliError) -> None: self.text_printer.print_error(error) def print_scan_results( - self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: dict[str, 'CliError'] | None = None ) -> None: if not errors and all(result.issue_detected == 0 for result in local_scan_results): self.console.print(self.NO_DETECTIONS_MESSAGE) diff --git a/cycode/cli/printers/text_printer.py b/cycode/cli/printers/text_printer.py index fe0adabf..7a88739f 100644 --- a/cycode/cli/printers/text_printer.py +++ b/cycode/cli/printers/text_printer.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from cycode.cli import consts from cycode.cli.cli_types import SeverityOption @@ -32,7 +32,7 @@ def print_error(self, error: CliError) -> None: self.console.print(f'[red]Error: {error.message}[/]', highlight=False) def print_scan_results( - self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: dict[str, 'CliError'] | None = None ) -> None: if not errors and all(result.issue_detected == 0 for result in local_scan_results): self.console.print(self.NO_DETECTIONS_MESSAGE) @@ -97,7 +97,7 @@ def __print_detection_code_segment(self, detection: 'Detection', document: Docum ) def print_report_urls_and_errors( - self, local_scan_results: list['LocalScanResult'], errors: Optional[dict[str, 'CliError']] = None + self, local_scan_results: list['LocalScanResult'], errors: dict[str, 'CliError'] | None = None ) -> None: report_urls = [scan_result.report_url for scan_result in local_scan_results if scan_result.report_url] @@ -110,7 +110,7 @@ def print_report_urls_and_errors( self.console.print(f'- {scan_id}: ', end='') self.print_error(error) - def print_report_urls(self, report_urls: list[str], aggregation_report_url: Optional[str] = None) -> None: + def print_report_urls(self, report_urls: list[str], aggregation_report_url: str | None = None) -> None: if not report_urls and not aggregation_report_url: return diff --git a/cycode/cli/printers/utils/code_snippet_syntax.py b/cycode/cli/printers/utils/code_snippet_syntax.py index d6ccd1a6..ceb2cc35 100644 --- a/cycode/cli/printers/utils/code_snippet_syntax.py +++ b/cycode/cli/printers/utils/code_snippet_syntax.py @@ -14,7 +14,7 @@ def _get_code_segment_start_line(detection_line: int, lines_to_display_before: int) -> int: start_line = detection_line - lines_to_display_before - return 0 if start_line < 0 else start_line + return max(start_line, 0) def get_detection_line(scan_type: str, detection: 'Detection') -> int: diff --git a/cycode/cli/printers/utils/detection_data.py b/cycode/cli/printers/utils/detection_data.py index 679429a3..51037627 100644 --- a/cycode/cli/printers/utils/detection_data.py +++ b/cycode/cli/printers/utils/detection_data.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from cycode.cli import consts @@ -7,7 +7,7 @@ from cycode.cyclient.models import Detection -def get_cwe_cve_link(cwe_cve: Optional[str]) -> Optional[str]: +def get_cwe_cve_link(cwe_cve: str | None) -> str | None: if not cwe_cve: return None @@ -63,7 +63,7 @@ def link(url: str, name: str) -> str: return '' -def get_detection_cwe_cve(scan_type: str, detection: 'Detection') -> Optional[str]: +def get_detection_cwe_cve(scan_type: str, detection: 'Detection') -> str | None: if scan_type == consts.SCA_SCAN_TYPE: return detection.detection_details.get('vulnerability_id') if scan_type == consts.SAST_SCAN_TYPE: diff --git a/cycode/cli/printers/utils/sca_ossf.py b/cycode/cli/printers/utils/sca_ossf.py index 11d3f662..b8d77ac6 100644 --- a/cycode/cli/printers/utils/sca_ossf.py +++ b/cycode/cli/printers/utils/sca_ossf.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any _MAINTAINED_CHECK_NAME = 'maintained' @@ -7,15 +7,15 @@ def _get_ossf_details(detection_details: dict) -> dict: return detection_details.get('ossf') or {} -def get_ossf_score(detection_details: dict) -> Optional[Any]: +def get_ossf_score(detection_details: dict) -> Any | None: return _get_ossf_details(detection_details).get('score') -def get_ossf_report_url(detection_details: dict) -> Optional[str]: +def get_ossf_report_url(detection_details: dict) -> str | None: return _get_ossf_details(detection_details).get('scorecard_report_url') -def get_maintained_score(detection_details: dict) -> Optional[Any]: +def get_maintained_score(detection_details: dict) -> Any | None: for check in _get_ossf_details(detection_details).get('checks') or []: if str(check.get('name', '')).lower() == _MAINTAINED_CHECK_NAME: return check.get('score') diff --git a/cycode/cli/printers/utils/sca_policy_details.py b/cycode/cli/printers/utils/sca_policy_details.py index 757c1be0..5e1550b1 100644 --- a/cycode/cli/printers/utils/sca_policy_details.py +++ b/cycode/cli/printers/utils/sca_policy_details.py @@ -1,4 +1,5 @@ -from typing import TYPE_CHECKING, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING from cycode.cli.consts import ( LICENSE_COMPLIANCE_POLICY_ID, diff --git a/cycode/cli/user_settings/config_file_manager.py b/cycode/cli/user_settings/config_file_manager.py index cfab38d2..1ae2dc67 100644 --- a/cycode/cli/user_settings/config_file_manager.py +++ b/cycode/cli/user_settings/config_file_manager.py @@ -1,6 +1,6 @@ import os from collections.abc import Hashable -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any, Union from cycode.cli.consts import CYCODE_CONFIGURATION_DIRECTORY from cycode.cli.user_settings.base_file_manager import BaseFileManager @@ -30,26 +30,26 @@ class ConfigFileManager(BaseFileManager): def __init__(self, path: Union['Path', str]) -> None: self.path = path - def get_api_url(self) -> Optional[Any]: + def get_api_url(self) -> Any | None: return self._get_value_from_environment_section(self.API_URL_FIELD_NAME) - def get_app_url(self) -> Optional[Any]: + def get_app_url(self) -> Any | None: return self._get_value_from_environment_section(self.APP_URL_FIELD_NAME) - def get_verbose_flag(self) -> Optional[Any]: + def get_verbose_flag(self) -> Any | None: return self._get_value_from_environment_section(self.VERBOSE_FIELD_NAME) def get_exclusions_by_scan_type(self, scan_type: str) -> dict[Hashable, Any]: exclusions_section = self._get_section(self.EXCLUSIONS_SECTION_NAME) return exclusions_section.get(scan_type, {}) - def get_max_commits(self, command_scan_type: str) -> Optional[Any]: + def get_max_commits(self, command_scan_type: str) -> Any | None: return self._get_value_from_command_scan_type_configuration(command_scan_type, self.MAX_COMMITS_FIELD_NAME) - def get_command_timeout(self, command_scan_type: str) -> Optional[Any]: + def get_command_timeout(self, command_scan_type: str) -> Any | None: return self._get_value_from_command_scan_type_configuration(command_scan_type, self.COMMAND_TIMEOUT_FIELD_NAME) - def get_exclude_detections_in_deleted_lines(self, command_scan_type: str) -> Optional[Any]: + def get_exclude_detections_in_deleted_lines(self, command_scan_type: str) -> Any | None: return self._get_value_from_command_scan_type_configuration( command_scan_type, self.EXCLUDE_DETECTIONS_IN_DELETED_LINES ) @@ -62,7 +62,7 @@ def update_app_base_url(self, app_url: str) -> None: update_data = {self.ENVIRONMENT_SECTION_NAME: {self.APP_URL_FIELD_NAME: app_url}} self.write_content_to_file(update_data) - def get_installation_id(self) -> Optional[str]: + def get_installation_id(self) -> str | None: return self._get_value_from_environment_section(self.INSTALLATION_ID_FIELD_NAME) def update_installation_id(self, installation_id: str) -> None: @@ -103,7 +103,7 @@ def _get_exclusions_by_exclusion_type(self, scan_type: str, exclusion_type: str) scan_type_exclusions = self.get_exclusions_by_scan_type(scan_type) return scan_type_exclusions.get(exclusion_type, []) - def _get_value_from_environment_section(self, field_name: str) -> Optional[Any]: + def _get_value_from_environment_section(self, field_name: str) -> Any | None: environment_section = self._get_section(self.ENVIRONMENT_SECTION_NAME) return environment_section.get(field_name) @@ -111,7 +111,7 @@ def _get_scan_configuration_by_scan_type(self, command_scan_type: str) -> dict[H scan_section = self._get_section(self.SCAN_SECTION_NAME) return scan_section.get(command_scan_type, {}) - def _get_value_from_command_scan_type_configuration(self, command_scan_type: str, field_name: str) -> Optional[Any]: + def _get_value_from_command_scan_type_configuration(self, command_scan_type: str, field_name: str) -> Any | None: command_scan_type_configuration = self._get_scan_configuration_by_scan_type(command_scan_type) return command_scan_type_configuration.get(field_name) diff --git a/cycode/cli/user_settings/configuration_manager.py b/cycode/cli/user_settings/configuration_manager.py index f80f9a6e..5747c12a 100644 --- a/cycode/cli/user_settings/configuration_manager.py +++ b/cycode/cli/user_settings/configuration_manager.py @@ -1,7 +1,7 @@ import os from functools import cache from pathlib import Path -from typing import Any, Optional +from typing import Any from uuid import uuid4 from cycode.cli import consts @@ -59,10 +59,10 @@ def get_verbose_flag(self) -> bool: verbose_flag_global_config = self.global_config_file_manager.get_verbose_flag() return verbose_flag_env_var or verbose_flag_local_config or verbose_flag_global_config - def get_api_url_from_environment_variables(self) -> Optional[str]: + def get_api_url_from_environment_variables(self) -> str | None: return self._get_value_from_environment_variables(consts.CYCODE_API_URL_ENV_VAR_NAME) - def get_app_url_from_environment_variables(self) -> Optional[str]: + def get_app_url_from_environment_variables(self) -> str | None: return self._get_value_from_environment_variables(consts.CYCODE_APP_URL_ENV_VAR_NAME) def get_verbose_flag_from_environment_variables(self) -> bool: @@ -94,13 +94,13 @@ def get_or_create_installation_id(self) -> str: return installation_id - def get_last_reported_activation_version(self, client: str) -> Optional[str]: + def get_last_reported_activation_version(self, client: str) -> str | None: return self.global_config_file_manager.get_last_reported_activation_versions().get(client) def update_last_reported_activation_version(self, client: str, version: str) -> None: self.global_config_file_manager.update_last_reported_activation_version(client, version) - def get_config_file_manager(self, scope: Optional[str] = None) -> ConfigFileManager: + def get_config_file_manager(self, scope: str | None = None) -> ConfigFileManager: if scope == 'local': return self.local_config_file_manager @@ -223,5 +223,5 @@ def get_should_exclude_detections_in_deleted_lines(self, command_scan_type: str) return consts.DEFAULT_EXCLUDE_DETECTIONS_IN_DELETED_LINES @staticmethod - def _get_value_from_environment_variables(env_var_name: str, default: Optional[Any] = None) -> Optional[Any]: + def _get_value_from_environment_variables(env_var_name: str, default: Any | None = None) -> Any | None: return os.getenv(env_var_name, default) diff --git a/cycode/cli/user_settings/credentials_manager.py b/cycode/cli/user_settings/credentials_manager.py index 9522981b..c44db151 100644 --- a/cycode/cli/user_settings/credentials_manager.py +++ b/cycode/cli/user_settings/credentials_manager.py @@ -1,6 +1,5 @@ import os from pathlib import Path -from typing import Optional from cycode.cli.config import ( CYCODE_CLIENT_ID_ENV_VAR_NAME, @@ -36,19 +35,19 @@ def get_credentials_from_environment_variables() -> tuple[str, str]: client_secret = os.getenv(CYCODE_CLIENT_SECRET_ENV_VAR_NAME) return client_id, client_secret - def get_credentials_from_file(self) -> tuple[Optional[str], Optional[str]]: + def get_credentials_from_file(self) -> tuple[str | None, str | None]: file_content = self.read_file() client_id = file_content.get(self.CLIENT_ID_FIELD_NAME) client_secret = file_content.get(self.CLIENT_SECRET_FIELD_NAME) return client_id, client_secret - def get_oidc_credentials_from_file(self) -> tuple[Optional[str], Optional[str]]: + def get_oidc_credentials_from_file(self) -> tuple[str | None, str | None]: file_content = self.read_file() client_id = file_content.get(self.CLIENT_ID_FIELD_NAME) id_token = file_content.get(self.ID_TOKEN_FIELD_NAME) return client_id, id_token - def get_oidc_credentials(self) -> tuple[Optional[str], Optional[str]]: + def get_oidc_credentials(self) -> tuple[str | None, str | None]: client_id = os.getenv(CYCODE_CLIENT_ID_ENV_VAR_NAME) id_token = os.getenv(CYCODE_ID_TOKEN_ENV_VAR_NAME) @@ -65,7 +64,7 @@ def update_credentials(self, client_id: str, client_secret: str) -> None: file_content_to_update = {self.CLIENT_ID_FIELD_NAME: client_id, self.CLIENT_SECRET_FIELD_NAME: client_secret} self.write_content_to_file(file_content_to_update) - def get_access_token(self) -> tuple[Optional[str], Optional[float], Optional[JwtCreator]]: + def get_access_token(self) -> tuple[str | None, float | None, JwtCreator | None]: file_content = self.read_file() access_token = file_content.get(self.ACCESS_TOKEN_FIELD_NAME) @@ -79,7 +78,7 @@ def get_access_token(self) -> tuple[Optional[str], Optional[float], Optional[Jwt return access_token, expires_in, creator def update_access_token( - self, access_token: Optional[str], expires_in: Optional[float], creator: Optional[JwtCreator] + self, access_token: str | None, expires_in: float | None, creator: JwtCreator | None ) -> None: file_content_to_update = { self.ACCESS_TOKEN_FIELD_NAME: access_token, diff --git a/cycode/cli/utils/get_api_client.py b/cycode/cli/utils/get_api_client.py index b69666d3..3612ff1d 100644 --- a/cycode/cli/utils/get_api_client.py +++ b/cycode/cli/utils/get_api_client.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Union import click @@ -21,10 +21,10 @@ def _get_cycode_client( create_client_func: callable, - client_id: Optional[str], - client_secret: Optional[str], + client_id: str | None, + client_secret: str | None, hide_response_log: bool, - id_token: Optional[str] = None, + id_token: str | None = None, ) -> Union['ScanClient', 'ReportClient', 'ImportSbomClient', 'AISecurityManagerClient']: if client_id and id_token: return create_client_func(client_id, None, hide_response_log, id_token) @@ -80,6 +80,6 @@ def _get_configured_credentials() -> tuple[str, str]: return credentials_manager.get_credentials() -def _get_configured_oidc_credentials() -> tuple[Optional[str], Optional[str]]: +def _get_configured_oidc_credentials() -> tuple[str | None, str | None]: credentials_manager = CredentialsManager() return credentials_manager.get_oidc_credentials() diff --git a/cycode/cli/utils/git_proxy.py b/cycode/cli/utils/git_proxy.py index 0aa9c78e..23412cb4 100644 --- a/cycode/cli/utils/git_proxy.py +++ b/cycode/cli/utils/git_proxy.py @@ -1,7 +1,7 @@ import types from abc import ABC, abstractmethod from functools import cache -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING _GIT_ERROR_MESSAGE = """ Cycode CLI needs the Git executable to be installed on the system. @@ -21,7 +21,7 @@ class GitProxyError(Exception): # GitPython runs `git version` on import, so it is imported on first use rather than at CLI startup @cache -def _import_git() -> Optional[types.ModuleType]: +def _import_git() -> types.ModuleType | None: try: import git except ImportError: @@ -31,7 +31,7 @@ def _import_git() -> Optional[types.ModuleType]: class _AbstractGitProxy(ABC): @abstractmethod - def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo': ... + def get_repo(self, path: 'PathLike | None' = None, *args, **kwargs) -> 'Repo': ... @abstractmethod def get_null_tree(self) -> object: ... @@ -44,7 +44,7 @@ def get_git_command_error(self) -> type[BaseException]: ... class _DummyGitProxy(_AbstractGitProxy): - def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo': + def get_repo(self, path: 'PathLike | None' = None, *args, **kwargs) -> 'Repo': raise RuntimeError(_GIT_ERROR_MESSAGE) def get_null_tree(self) -> object: @@ -61,7 +61,7 @@ class _GitProxy(_AbstractGitProxy): def __init__(self, git_module: types.ModuleType) -> None: self._git = git_module - def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo': + def get_repo(self, path: 'PathLike | None' = None, *args, **kwargs) -> 'Repo': return self._git.Repo(path, *args, **kwargs) def get_null_tree(self) -> object: @@ -74,7 +74,7 @@ def get_git_command_error(self) -> type[BaseException]: return self._git.GitCommandError -def get_git_proxy(git_module: Optional[types.ModuleType]) -> _AbstractGitProxy: +def get_git_proxy(git_module: types.ModuleType | None) -> _AbstractGitProxy: return _GitProxy(git_module) if git_module else _DummyGitProxy() @@ -82,7 +82,7 @@ class GitProxyManager(_AbstractGitProxy): """We are using this manager for easy unit testing and mocking of the git module.""" def __init__(self) -> None: - self._git_proxy: Optional[_AbstractGitProxy] = None + self._git_proxy: _AbstractGitProxy | None = None def _get_git_proxy(self) -> _AbstractGitProxy: if self._git_proxy is None: @@ -95,7 +95,7 @@ def _set_dummy_git_proxy(self) -> None: def _set_git_proxy(self) -> None: self._git_proxy = _GitProxy(_import_git()) - def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo': + def get_repo(self, path: 'PathLike | None' = None, *args, **kwargs) -> 'Repo': return self._get_git_proxy().get_repo(path, *args, **kwargs) def get_null_tree(self) -> object: diff --git a/cycode/cli/utils/host_info.py b/cycode/cli/utils/host_info.py index 5ea3e62d..79da200a 100644 --- a/cycode/cli/utils/host_info.py +++ b/cycode/cli/utils/host_info.py @@ -7,15 +7,15 @@ import sys import tempfile from pathlib import Path -from typing import Any, Optional +from typing import Any from cycode.cli.consts import CYCODE_CONFIGURATION_DIRECTORY from cycode.logger import get_logger logger = get_logger('HOST INFO') -pythoncom: Optional[Any] = None -win32com_client: Optional[Any] = None +pythoncom: Any | None = None +win32com_client: Any | None = None if sys.platform == 'win32': try: import pythoncom @@ -30,31 +30,22 @@ _PLATFORM_NAMES = {'Darwin': 'macOS', 'Windows': 'Windows', 'Linux': 'Linux'} -def _run(command: list, timeout: int = _SUBPROCESS_TIMEOUT_SEC) -> Optional[str]: +def _run(command: list, timeout: int = _SUBPROCESS_TIMEOUT_SEC) -> str | None: """Run a command and return its stripped stdout. Never raises; returns None on any error.""" try: - result = subprocess.run(command, capture_output=True, text=True, timeout=timeout) # noqa: S603 + result = subprocess.run(command, capture_output=True, text=True, timeout=timeout, check=False) # noqa: S603 return result.stdout.strip() or None except Exception as e: logger.debug('Failed to run command %s', command, exc_info=e) return None -def _read_text_file(path: str) -> Optional[str]: - """Read and strip a text file. Never raises; returns None if it can't be read.""" - try: - with open(path) as text_file: - return text_file.read().strip() or None - except OSError: - return None - - def is_64bit() -> bool: """Whether the running Python interpreter is 64-bit (not the OS).""" return sys.maxsize > 2**32 -def get_hostname() -> Optional[str]: +def get_hostname() -> str | None: try: return socket.gethostname() or None except Exception as e: @@ -62,7 +53,7 @@ def get_hostname() -> Optional[str]: return None -def get_platform_name() -> Optional[str]: +def get_platform_name() -> str | None: try: system = platform.system() return _PLATFORM_NAMES.get(system, system or None) @@ -71,7 +62,7 @@ def get_platform_name() -> Optional[str]: return None -def get_os_version() -> Optional[str]: +def get_os_version() -> str | None: try: system = platform.system() if system == 'Darwin': @@ -86,26 +77,18 @@ def get_os_version() -> Optional[str]: return None -def _get_linux_os_version() -> Optional[str]: - freedesktop_os_release = getattr(platform, 'freedesktop_os_release', None) # Python 3.10+ - if freedesktop_os_release is not None: - try: - version_id = freedesktop_os_release().get('VERSION_ID') - if version_id: - return version_id - except OSError: - pass - - os_release = _read_text_file('/etc/os-release') # Python 3.9 fallback: parse manually - if os_release: - for line in os_release.splitlines(): - if line.startswith('VERSION_ID='): - return line.split('=', 1)[1].strip().strip('"') or None +def _get_linux_os_version() -> str | None: + try: + version_id = platform.freedesktop_os_release().get('VERSION_ID') + if version_id: + return version_id + except OSError: + pass return platform.release() or None -def get_last_login_user() -> Optional[str]: +def get_last_login_user() -> str | None: try: return getpass.getuser() or None except Exception as e: @@ -113,7 +96,7 @@ def get_last_login_user() -> Optional[str]: return None -def get_serial_number() -> Optional[str]: +def get_serial_number() -> str | None: # The serial is immutable hardware info, but resolving it shells out (ioreg/WMI) # and this runs in a fresh process per AI hook event - cache it on disk. cached = _read_serial_number_cache() @@ -126,7 +109,7 @@ def get_serial_number() -> Optional[str]: return serial -def _resolve_serial_number() -> Optional[str]: +def _resolve_serial_number() -> str | None: try: system = platform.system() if system == 'Darwin': @@ -142,7 +125,7 @@ def _serial_number_cache_path() -> Path: return Path.home() / CYCODE_CONFIGURATION_DIRECTORY / _DEVICE_ID_CACHE_FILE_NAME -def _read_serial_number_cache() -> Optional[str]: +def _read_serial_number_cache() -> str | None: try: return _serial_number_cache_path().read_text(encoding='utf-8').strip() or None except Exception: @@ -170,7 +153,7 @@ def _write_serial_number_cache(serial: str) -> None: logger.debug('Failed to cache serial number', exc_info=e) -def _get_macos_serial_number() -> Optional[str]: +def _get_macos_serial_number() -> str | None: output = _run(['ioreg', '-c', 'IOPlatformExpertDevice', '-d', '2']) if not output: return None @@ -178,7 +161,7 @@ def _get_macos_serial_number() -> Optional[str]: return match.group(1) if match else None -def _get_windows_serial_number() -> Optional[str]: +def _get_windows_serial_number() -> str | None: """Read the OEM serial over WMI.""" if pythoncom is None or win32com_client is None: return None diff --git a/cycode/cli/utils/ignore_utils.py b/cycode/cli/utils/ignore_utils.py index 98126658..b20e7b97 100644 --- a/cycode/cli/utils/ignore_utils.py +++ b/cycode/cli/utils/ignore_utils.py @@ -43,8 +43,6 @@ from typing import ( Any, BinaryIO, - Optional, - Union, ) @@ -205,9 +203,9 @@ def match(self, path: bytes) -> bool: class IgnoreFilter: def __init__( self, - patterns: Iterable[Union[str, bytes]], + patterns: Iterable[str | bytes], ignore_case: bool = False, - path: Optional[Union[PathLike, str]] = None, + path: PathLike | str | None = None, ) -> None: if hasattr(path, '__fspath__'): path = path.__fspath__() @@ -227,13 +225,13 @@ def to_dict(self) -> dict[str, Any]: d['path'] = path return d - def append_pattern(self, pattern: Union[str, bytes]) -> None: + def append_pattern(self, pattern: str | bytes) -> None: """Add a pattern to the set.""" if isinstance(pattern, str): pattern = bytes(pattern, 'utf-8') self._patterns.append(Pattern(pattern, self._ignore_case)) - def find_matching(self, path: Union[bytes, str]) -> Iterable[Pattern]: + def find_matching(self, path: bytes | str) -> Iterable[Pattern]: """Yield all matching patterns for path. Args: @@ -248,7 +246,7 @@ def find_matching(self, path: Union[bytes, str]) -> Iterable[Pattern]: if pattern.match(path): yield pattern - def is_ignored(self, path: Union[bytes, str]) -> Optional[bool]: + def is_ignored(self, path: bytes | str) -> bool | None: """Check whether a path is ignored. For directories, include a trailing slash. @@ -264,7 +262,7 @@ def is_ignored(self, path: Union[bytes, str]) -> Optional[bool]: return status @classmethod - def from_path(cls, path: Union[PathLike, str], ignore_case: bool = False) -> 'IgnoreFilter': + def from_path(cls, path: PathLike | str, ignore_case: bool = False) -> 'IgnoreFilter': if hasattr(path, '__fspath__'): path = path.__fspath__() with open(path, 'rb') as f: @@ -284,7 +282,7 @@ def __init__( self, path: str, global_filters: list[IgnoreFilter], - ignore_file_name: Optional[str] = None, + ignore_file_name: str | None = None, ignore_case: bool = False, ) -> None: if hasattr(path, '__fspath__'): @@ -317,14 +315,14 @@ def path(self) -> str: return self._top_path @property - def ignore_file_name(self) -> Optional[str]: + def ignore_file_name(self) -> str | None: return self._ignore_file_name @property def ignore_case(self) -> bool: return self._ignore_case - def _load_path(self, path: str) -> Optional[IgnoreFilter]: + def _load_path(self, path: str) -> IgnoreFilter | None: try: return self._path_filters[path] except KeyError: @@ -370,7 +368,7 @@ def _find_matching(self, path: str) -> Iterable[Pattern]: filters.insert(0, (i, ignore_filter)) return iter(matches) - def is_ignored(self, path: str) -> Optional[bool]: + def is_ignored(self, path: str) -> bool | None: """Check whether a path is ignored. Args: @@ -425,9 +423,9 @@ def walk_with_ignored( def build( cls, path: str, - global_ignore_file_paths: Optional[Iterable[str]] = None, - global_patterns: Optional[Iterable[Union[str, bytes]]] = None, - ignore_file_name: Optional[str] = None, + global_ignore_file_paths: Iterable[str] | None = None, + global_patterns: Iterable[str | bytes] | None = None, + ignore_file_name: str | None = None, ignore_case: bool = False, ) -> 'IgnoreFilterManager': """Create a IgnoreFilterManager from patterns and paths. diff --git a/cycode/cli/utils/jwt_utils.py b/cycode/cli/utils/jwt_utils.py index 21f767d0..38c7476b 100644 --- a/cycode/cli/utils/jwt_utils.py +++ b/cycode/cli/utils/jwt_utils.py @@ -1,11 +1,9 @@ -from typing import Optional - import jwt _JWT_PAYLOAD_POSSIBLE_USER_ID_FIELD_NAMES = ('userId', 'internalId', 'token-user-id') -def decode_jwt_unverified(token: str) -> Optional[dict]: +def decode_jwt_unverified(token: str) -> dict | None: """Return JWT claims without signature verification, or None if the token is unreadable.""" try: return jwt.decode(token, options={'verify_signature': False}) @@ -13,7 +11,7 @@ def decode_jwt_unverified(token: str) -> Optional[dict]: return None -def get_user_and_tenant_ids_from_access_token(access_token: str) -> tuple[Optional[str], Optional[str]]: +def get_user_and_tenant_ids_from_access_token(access_token: str) -> tuple[str | None, str | None]: payload = jwt.decode(access_token, options={'verify_signature': False}) user_id = None diff --git a/cycode/cli/utils/path_utils.py b/cycode/cli/utils/path_utils.py index 87d3ee4e..97520103 100644 --- a/cycode/cli/utils/path_utils.py +++ b/cycode/cli/utils/path_utils.py @@ -3,7 +3,7 @@ import tempfile from functools import cache from pathlib import PurePath, PurePosixPath, PureWindowsPath -from typing import TYPE_CHECKING, AnyStr, Optional, Union +from typing import TYPE_CHECKING, AnyStr, Union import typer @@ -31,7 +31,7 @@ def get_absolute_path(path: str) -> str: return os.path.abspath(path) -def _get_starting_chunk(filename: str, length: int = 1024) -> Optional[bytes]: +def _get_starting_chunk(filename: str, length: int = 1024) -> bytes | None: # We are using our own implementation of get_starting_chunk # because the original one from binaryornot uses print()... @@ -79,7 +79,7 @@ def join_paths(path: str, filename: str) -> str: return os.path.join(path, filename) -def get_file_content(file_path: Union[str, 'PathLike']) -> Optional[AnyStr]: +def get_file_content(file_path: Union[str, 'PathLike']) -> AnyStr | None: try: with open(file_path, encoding='UTF-8') as f: return f.read() @@ -115,7 +115,7 @@ def quarantine_corrupt_file(filename: str) -> None: logger.warning('Failed to quarantine corrupt file, %s', {'filename': filename}, exc_info=e) -def load_json(txt: str) -> Optional[dict]: +def load_json(txt: str) -> dict | None: try: return json.loads(txt) except json.JSONDecodeError: @@ -152,7 +152,7 @@ def concat_unique_id(filename: str, unique_id: str) -> str: return str(PurePosixPath(unique_id, *_to_relative_posix_parts(filename))) -def _as_scan_root(value: object) -> Optional[str]: +def _as_scan_root(value: object) -> str | None: if isinstance(value, (str, os.PathLike)): return os.fspath(value) or None @@ -181,7 +181,7 @@ def get_scan_roots_from_context(ctx: typer.Context) -> tuple[str, ...]: return tuple(dict.fromkeys(scan_roots)) -def get_path_from_context(ctx: typer.Context) -> Optional[str]: +def get_path_from_context(ctx: typer.Context) -> str | None: scan_roots = get_scan_roots_from_context(ctx) return scan_roots[0] if scan_roots else None diff --git a/cycode/cli/utils/progress_bar.py b/cycode/cli/utils/progress_bar.py index 7c2de487..e58e8ede 100644 --- a/cycode/cli/utils/progress_bar.py +++ b/cycode/cli/utils/progress_bar.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from enum import auto -from typing import NamedTuple, Optional +from typing import NamedTuple from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn, TimeElapsedColumn @@ -109,7 +109,7 @@ def set_section_length(self, section: 'ProgressBarSection', length: int = 0) -> def update(self, section: 'ProgressBarSection') -> None: ... @abstractmethod - def update_right_side_label(self, label: Optional[str] = None) -> None: ... + def update_right_side_label(self, label: str | None = None) -> None: ... class DummyProgressBar(BaseProgressBar): @@ -128,7 +128,7 @@ def set_section_length(self, section: 'ProgressBarSection', length: int = 0) -> def update(self, section: 'ProgressBarSection') -> None: pass - def update_right_side_label(self, label: Optional[str] = None) -> None: + def update_right_side_label(self, label: str | None = None) -> None: pass @@ -240,7 +240,7 @@ def update(self, section: 'ProgressBarSection', value: int = 1) -> None: self._increment_progress(section) self._maybe_update_current_section() - def update_right_side_label(self, label: Optional[str] = None) -> None: + def update_right_side_label(self, label: str | None = None) -> None: self._current_right_side_label = f'({label})' if label else '' self._progress_bar_update() diff --git a/cycode/cli/utils/scan_batch.py b/cycode/cli/utils/scan_batch.py index 97e58bc7..5375b7a4 100644 --- a/cycode/cli/utils/scan_batch.py +++ b/cycode/cli/utils/scan_batch.py @@ -1,6 +1,7 @@ import os +from collections.abc import Callable from multiprocessing.pool import ThreadPool -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING from cycode.cli import consts from cycode.cli.models import Document diff --git a/cycode/cli/utils/scan_utils.py b/cycode/cli/utils/scan_utils.py index 951d6cc4..821a1ac4 100644 --- a/cycode/cli/utils/scan_utils.py +++ b/cycode/cli/utils/scan_utils.py @@ -1,5 +1,5 @@ import os -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from uuid import UUID, uuid4 import typer @@ -26,7 +26,7 @@ def is_scan_failed(ctx: typer.Context) -> bool: def is_cycodeignore_allowed_by_scan_config(ctx: typer.Context) -> bool: - scan_config: Optional[ScanConfiguration] = ctx.obj.get('scan_config') + scan_config: ScanConfiguration | None = ctx.obj.get('scan_config') return scan_config.is_cycode_ignore_allowed if scan_config else True diff --git a/cycode/cli/utils/shell_executor.py b/cycode/cli/utils/shell_executor.py index b39d2a0b..d0c2f829 100644 --- a/cycode/cli/utils/shell_executor.py +++ b/cycode/cli/utils/shell_executor.py @@ -1,6 +1,5 @@ import subprocess import time -from typing import Optional, Union import click import typer @@ -14,11 +13,11 @@ def shell( - command: Union[str, list[str]], + command: str | list[str], timeout: int = _SUBPROCESS_DEFAULT_TIMEOUT_SEC, - working_directory: Optional[str] = None, + working_directory: str | None = None, silent_exc_info: bool = False, -) -> Optional[str]: +) -> str | None: logger.debug('Executing shell command: %s', command) try: diff --git a/cycode/cli/utils/task_timer.py b/cycode/cli/utils/task_timer.py index 4b5e903e..b7702504 100644 --- a/cycode/cli/utils/task_timer.py +++ b/cycode/cli/utils/task_timer.py @@ -1,11 +1,11 @@ from _thread import interrupt_main +from collections.abc import Callable from threading import Event, Thread from types import TracebackType -from typing import Callable, Optional class FunctionContext: - def __init__(self, function: Callable, args: Optional[list] = None, kwargs: Optional[dict] = None) -> None: + def __init__(self, function: Callable, args: list | None = None, kwargs: dict | None = None) -> None: self.function = function self.args = args or [] self.kwargs = kwargs or {} @@ -54,7 +54,7 @@ class TimeoutAfter: the default option is to interrupt main thread """ - def __init__(self, timeout: int, quit_function: Optional[FunctionContext] = None) -> None: + def __init__(self, timeout: int, quit_function: FunctionContext | None = None) -> None: self.timeout = timeout self._quit_function = quit_function or FunctionContext(function=self.timeout_function) self.timer = TimerThread(timeout, quit_function=self._quit_function) @@ -64,7 +64,7 @@ def __enter__(self) -> None: self.timer.start() def __exit__( - self, exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType] + self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None ) -> None: if self.timeout: self.timer.stop() diff --git a/cycode/cli/utils/trust_store.py b/cycode/cli/utils/trust_store.py index 55bcaff3..53873bf4 100644 --- a/cycode/cli/utils/trust_store.py +++ b/cycode/cli/utils/trust_store.py @@ -1,21 +1,12 @@ -import sys - from cycode import config from cycode.cli import consts from cycode.logger import get_logger logger = get_logger('Trust Store') -# truststore requires Python 3.10+, so on 3.9 the OS trust store is unavailable -_MIN_PYTHON_VERSION = (3, 10) - _installed = False -def is_supported() -> bool: - return sys.version_info >= _MIN_PYTHON_VERSION - - def is_enabled() -> bool: return config.get_val_as_bool(consts.ENABLE_TRUSTSTORE_ENV_VAR_NAME) @@ -45,14 +36,6 @@ def install() -> bool: ) return False - if not is_supported(): - logger.warning( - 'OS trust store requires Python %s+, using the bundled CA store (certifi). Current version: %s', - '.'.join(map(str, _MIN_PYTHON_VERSION)), - '.'.join(map(str, sys.version_info[:3])), - ) - return False - try: import truststore diff --git a/cycode/cli/utils/url_utils.py b/cycode/cli/utils/url_utils.py index 91e50f77..2f7eacea 100644 --- a/cycode/cli/utils/url_utils.py +++ b/cycode/cli/utils/url_utils.py @@ -1,4 +1,3 @@ -from typing import Optional from urllib.parse import urlparse, urlunparse from cycode.logger import get_logger @@ -6,7 +5,7 @@ logger = get_logger('URL Utils') -def sanitize_repository_url(url: Optional[str]) -> Optional[str]: +def sanitize_repository_url(url: str | None) -> str | None: """Remove credentials (username, password, tokens) from repository URL. This function sanitizes repository URLs to prevent sending PAT tokens or other diff --git a/cycode/cli/utils/version_checker.py b/cycode/cli/utils/version_checker.py index 24146989..63c554b0 100644 --- a/cycode/cli/utils/version_checker.py +++ b/cycode/cli/utils/version_checker.py @@ -2,7 +2,6 @@ import re import time from pathlib import Path -from typing import Optional from cycode.cli.console import console from cycode.cli.user_settings.configuration_manager import ConfigurationManager @@ -19,7 +18,7 @@ def _compare_versions( current_is_pre: bool, latest_is_pre: bool, latest_version: str, -) -> Optional[str]: +) -> str | None: """Compare version numbers and determine if an update is needed. Implements version comparison logic with special handling for pre-release versions: @@ -43,7 +42,7 @@ def _compare_versions( return None # Compare version numbers - for current, latest in zip(current_parts, latest_parts): + for current, latest in zip(current_parts, latest_parts, strict=False): if latest > current: return latest_version if current > latest: @@ -78,7 +77,7 @@ def __init__(self) -> None: config_dir = configuration_manager.global_config_file_manager.get_config_directory_path() self.cache_file = Path(config_dir) / '.version_check' - def get_latest_version(self) -> Optional[str]: + def get_latest_version(self) -> str | None: """Fetch the latest version of the package from PyPI. Makes an HTTP request to PyPI's JSON API to get the latest version information. @@ -160,7 +159,7 @@ def _update_last_check(self) -> None: except Exception as e: logger.debug('Failed to update version check cache file: %s', {'file': self.cache_file}, exc_info=e) - def check_for_update(self, current_version: str, use_cache: bool = True) -> Optional[str]: + def check_for_update(self, current_version: str, use_cache: bool = True) -> str | None: """Check if an update is available for the current version. Respects the update check frequency (daily/weekly) based on the version type diff --git a/cycode/cli/utils/yaml_utils.py b/cycode/cli/utils/yaml_utils.py index 73a61673..465bfa9b 100644 --- a/cycode/cli/utils/yaml_utils.py +++ b/cycode/cli/utils/yaml_utils.py @@ -15,7 +15,7 @@ def _deep_update(source: dict[Hashable, Any], overrides: dict[Hashable, Any]) -> if isinstance(value, dict) and value: source[key] = _deep_update(source.get(key, {}), value) else: - source[key] = overrides[key] + source[key] = value return source diff --git a/cycode/config.py b/cycode/config.py index f4306b31..77326064 100644 --- a/cycode/config.py +++ b/cycode/config.py @@ -1,6 +1,5 @@ import logging import os -from typing import Optional from urllib.parse import urlparse from cycode.cli import consts @@ -26,7 +25,7 @@ def get_val_as_bool(key: str, default: bool = False) -> bool: return configuration[key].lower() in {'true', '1', 'yes', 'y', 'on', 'enabled'} -def get_val_as_int(key: str) -> Optional[int]: +def get_val_as_int(key: str) -> int | None: val = configuration.get(key) if not val: return None diff --git a/cycode/cyclient/ai_security_manager_client.py b/cycode/cyclient/ai_security_manager_client.py index ef536c91..4d00a50a 100644 --- a/cycode/cyclient/ai_security_manager_client.py +++ b/cycode/cyclient/ai_security_manager_client.py @@ -1,6 +1,6 @@ """Client for AI Security Manager service.""" -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError from cycode.cyclient.cycode_client_base import CycodeClientBase @@ -32,7 +32,7 @@ def _build_endpoint_path(self, path: str) -> str: return f'{service_name}/{path}' return path - def create_conversation(self, payload: 'AIHookPayload') -> Optional[str]: + def create_conversation(self, payload: 'AIHookPayload') -> str | None: """Creates an AI conversation from hook payload.""" conversation_id = payload.conversation_id if not conversation_id: @@ -63,10 +63,10 @@ def create_event( payload: 'AIHookPayload', event_type: 'AiHookEventType', outcome: 'AIHookOutcome', - scan_id: Optional[str] = None, - block_reason: Optional['BlockReason'] = None, - error_message: Optional[str] = None, - file_path: Optional[str] = None, + scan_id: str | None = None, + block_reason: 'BlockReason | None' = None, + error_message: str | None = None, + file_path: str | None = None, ) -> None: """Create an AI hook event from hook payload.""" conversation_id = payload.conversation_id @@ -95,7 +95,7 @@ def create_event( logger.debug('Failed to create AI hook event', exc_info=e) # Don't fail the hook if tracking fails - def get_resolved_guardrails(self) -> Optional[dict]: + def get_resolved_guardrails(self) -> dict | None: """Fetch the tenant's resolved guardrail config (per-agent modes + sensitive-path globs).""" try: response = self.client.get(self._build_endpoint_path(self._RESOLVED_GUARDRAILS_PATH)) @@ -106,16 +106,16 @@ def get_resolved_guardrails(self) -> Optional[dict]: def report_session_context( self, - hostname: Optional[str] = None, - platform_name: Optional[str] = None, - os_version: Optional[str] = None, - serial_number: Optional[str] = None, - last_login_user: Optional[str] = None, - config_files: Optional[list[dict]] = None, - enabled_plugins: Optional[dict] = None, - skill_files: Optional[list[dict]] = None, - user_email: Optional[str] = None, - ) -> Optional[SessionContextResponse]: + hostname: str | None = None, + platform_name: str | None = None, + os_version: str | None = None, + serial_number: str | None = None, + last_login_user: str | None = None, + config_files: list[dict] | None = None, + enabled_plugins: dict | None = None, + skill_files: list[dict] | None = None, + user_email: str | None = None, + ) -> SessionContextResponse | None: """Report session context to the backend. Returns None when the report was not accepted.""" body: dict = { 'hostname': hostname, diff --git a/cycode/cyclient/auth_client.py b/cycode/cyclient/auth_client.py index 1df7ad9b..9faee5f9 100644 --- a/cycode/cyclient/auth_client.py +++ b/cycode/cyclient/auth_client.py @@ -1,5 +1,3 @@ -from typing import Optional - from requests import Request, Response from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, RequestHttpError @@ -24,7 +22,7 @@ def start_session(self, code_challenge: str) -> models.AuthenticationSession: response = self.cycode_client.post(url_path=path, body=body) return self.parse_start_session_response(response) - def get_api_token(self, session_id: str, code_verifier: str) -> Optional[models.ApiTokenGenerationPollingResponse]: + def get_api_token(self, session_id: str, code_verifier: str) -> models.ApiTokenGenerationPollingResponse | None: path = f'{self.AUTH_CONTROLLER_PATH}/token' body = {'session_id': session_id, 'code_verifier': code_verifier} try: @@ -40,7 +38,7 @@ def parse_start_session_response(response: Response) -> models.AuthenticationSes return models.AuthenticationSessionSchema().load(response.json()) @staticmethod - def parse_api_token_polling_response(response: Response) -> Optional[models.ApiTokenGenerationPollingResponse]: + def parse_api_token_polling_response(response: Response) -> models.ApiTokenGenerationPollingResponse | None: try: return models.ApiTokenGenerationPollingResponseSchema().load(response.json()) except Exception: diff --git a/cycode/cyclient/base_token_auth_client.py b/cycode/cyclient/base_token_auth_client.py index cdadf98d..23d7995e 100644 --- a/cycode/cyclient/base_token_auth_client.py +++ b/cycode/cyclient/base_token_auth_client.py @@ -2,7 +2,7 @@ import time from abc import ABC, abstractmethod from threading import Lock -from typing import Any, Optional +from typing import Any import arrow from requests import Response @@ -97,7 +97,7 @@ def refresh_access_token(self) -> None: jwt_creator = self._create_jwt_creator() self._credentials_manager.update_access_token(self._access_token, self._expires_in.timestamp(), jwt_creator) - def get_request_headers(self, additional_headers: Optional[dict] = None, without_auth: bool = False) -> dict: + def get_request_headers(self, additional_headers: dict | None = None, without_auth: bool = False) -> dict: headers = super().get_request_headers(additional_headers=additional_headers) if not without_auth: diff --git a/cycode/cyclient/client_creator.py b/cycode/cyclient/client_creator.py index c26795c7..565d1847 100644 --- a/cycode/cyclient/client_creator.py +++ b/cycode/cyclient/client_creator.py @@ -1,5 +1,3 @@ -from typing import Optional - from cycode.cyclient.ai_security_manager_client import AISecurityManagerClient from cycode.cyclient.ai_security_manager_service_config import ( DefaultAISecurityManagerServiceConfig, @@ -17,7 +15,7 @@ def create_scan_client( - client_id: str, client_secret: Optional[str] = None, hide_response_log: bool = False, id_token: Optional[str] = None + client_id: str, client_secret: str | None = None, hide_response_log: bool = False, id_token: str | None = None ) -> ScanClient: if dev_mode: client = CycodeDevBasedClient(DEV_CYCODE_API_URL) @@ -33,7 +31,7 @@ def create_scan_client( def create_report_client( - client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None + client_id: str, client_secret: str | None = None, _: bool = False, id_token: str | None = None ) -> ReportClient: if dev_mode: client = CycodeDevBasedClient(DEV_CYCODE_API_URL) @@ -45,7 +43,7 @@ def create_report_client( def create_import_sbom_client( - client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None + client_id: str, client_secret: str | None = None, _: bool = False, id_token: str | None = None ) -> ImportSbomClient: if dev_mode: client = CycodeDevBasedClient(DEV_CYCODE_API_URL) @@ -57,7 +55,7 @@ def create_import_sbom_client( def create_ai_security_manager_client( - client_id: str, client_secret: Optional[str] = None, _: bool = False, id_token: Optional[str] = None + client_id: str, client_secret: str | None = None, _: bool = False, id_token: str | None = None ) -> AISecurityManagerClient: if dev_mode: client = CycodeDevBasedClient(DEV_CYCODE_API_URL) diff --git a/cycode/cyclient/cycode_client_base.py b/cycode/cyclient/cycode_client_base.py index 4e7eebe4..f2dfc020 100644 --- a/cycode/cyclient/cycode_client_base.py +++ b/cycode/cyclient/cycode_client_base.py @@ -2,8 +2,9 @@ import os import platform import ssl +from collections.abc import Callable from io import BytesIO -from typing import TYPE_CHECKING, Callable, ClassVar, Optional +from typing import TYPE_CHECKING, ClassVar import requests from requests import Response, exceptions @@ -114,7 +115,7 @@ def _should_retry_exception(exception: BaseException) -> bool: class UploadProgressTracker: """File-like wrapper that tracks bytes read during upload and fires a progress callback.""" - def __init__(self, data: bytes, callback: Optional[Callable[[int, int], None]]) -> None: + def __init__(self, data: bytes, callback: Callable[[int, int], None] | None) -> None: self._io = BytesIO(data) self._callback = callback self.bytes_read = 0 @@ -146,13 +147,13 @@ def reset_user_agent() -> None: def enrich_user_agent(user_agent_suffix: str) -> None: CycodeClientBase.MANDATORY_HEADERS['User-Agent'] += f' {user_agent_suffix}' - def post(self, url_path: str, body: Optional[dict] = None, headers: Optional[dict] = None, **kwargs) -> Response: + def post(self, url_path: str, body: dict | None = None, headers: dict | None = None, **kwargs) -> Response: return self._execute(method='post', endpoint=url_path, json=body, headers=headers, **kwargs) - def put(self, url_path: str, body: Optional[dict] = None, headers: Optional[dict] = None, **kwargs) -> Response: + def put(self, url_path: str, body: dict | None = None, headers: dict | None = None, **kwargs) -> Response: return self._execute(method='put', endpoint=url_path, json=body, headers=headers, **kwargs) - def get(self, url_path: str, headers: Optional[dict] = None, **kwargs) -> Response: + def get(self, url_path: str, headers: dict | None = None, **kwargs) -> Response: return self._execute(method='get', endpoint=url_path, headers=headers, **kwargs) def post_multipart( @@ -160,7 +161,7 @@ def post_multipart( url_path: str, form_fields: dict, files: dict, - on_upload_progress: Optional[Callable[[int, int], None]] = None, + on_upload_progress: Callable[[int, int], None] | None = None, hide_response_content_log: bool = False, ) -> Response: """POST a multipart form body with optional upload progress tracking and retry.""" @@ -191,7 +192,7 @@ def _send_multipart( url: str, body: bytes, content_type: str, - on_upload_progress: Optional[Callable[[int, int], None]], + on_upload_progress: Callable[[int, int], None] | None, hide_response_content_log: bool, ) -> Response: # Wrap the body in a fresh tracker each attempt so bytes_read starts from zero. @@ -227,7 +228,7 @@ def _execute( self, method: str, endpoint: str, - headers: Optional[dict] = None, + headers: dict | None = None, without_auth: bool = False, hide_response_content_log: bool = False, **kwargs, @@ -253,7 +254,7 @@ def _execute( except Exception as e: self._handle_exception(e) - def get_request_headers(self, additional_headers: Optional[dict] = None, **kwargs) -> dict[str, str]: + def get_request_headers(self, additional_headers: dict | None = None, **kwargs) -> dict[str, str]: if additional_headers is None: return self.MANDATORY_HEADERS.copy() return {**self.MANDATORY_HEADERS, **additional_headers} diff --git a/cycode/cyclient/cycode_dev_based_client.py b/cycode/cyclient/cycode_dev_based_client.py index d8fe1cab..82aeeff3 100644 --- a/cycode/cyclient/cycode_dev_based_client.py +++ b/cycode/cyclient/cycode_dev_based_client.py @@ -1,5 +1,3 @@ -from typing import Optional - from cycode.cyclient.config import dev_tenant_id from cycode.cyclient.cycode_client_base import CycodeClientBase @@ -12,7 +10,7 @@ class CycodeDevBasedClient(CycodeClientBase): def __init__(self, api_url: str) -> None: super().__init__(api_url) - def get_request_headers(self, additional_headers: Optional[dict] = None, **_) -> dict[str, str]: + def get_request_headers(self, additional_headers: dict | None = None, **_) -> dict[str, str]: headers = super().get_request_headers(additional_headers=additional_headers) headers['X-Tenant-Id'] = dev_tenant_id diff --git a/cycode/cyclient/headers.py b/cycode/cyclient/headers.py index 937f4333..7bd72cc8 100644 --- a/cycode/cyclient/headers.py +++ b/cycode/cyclient/headers.py @@ -1,5 +1,4 @@ import platform -from typing import Optional from uuid import uuid4 from cycode import __version__ @@ -25,7 +24,7 @@ def get_cli_user_agent() -> str: class _CorrelationId: - _id: Optional[str] = None + _id: str | None = None def get_correlation_id(self) -> str: """Get correlation ID. diff --git a/cycode/cyclient/import_sbom_client.py b/cycode/cyclient/import_sbom_client.py index d8107cf5..126598cb 100644 --- a/cycode/cyclient/import_sbom_client.py +++ b/cycode/cyclient/import_sbom_client.py @@ -1,6 +1,5 @@ import dataclasses from pathlib import Path -from typing import Optional from requests import Response @@ -15,8 +14,8 @@ class ImportSbomParameters: Name: str Vendor: str BusinessImpact: BusinessImpactOption - Labels: Optional[list[str]] - Owners: Optional[list[str]] + Labels: list[str] | None + Owners: list[str] | None def _owners_to_ids(self) -> list[str]: return [] @@ -71,7 +70,7 @@ def _get_user_id_by_email(self, email: str) -> str: member_details = self.parse_requested_member_details_response(response) if not member_details.items: - raise Exception( + raise Exception( # noqa: TRY002 f"Failed to find user with email '{email}'. Verify this email is registered to Cycode platform" ) return member_details.items.pop(0).external_id diff --git a/cycode/cyclient/models.py b/cycode/cyclient/models.py index 1d20c4ab..ba9d807c 100644 --- a/cycode/cyclient/models.py +++ b/cycode/cyclient/models.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from enum import Enum -from typing import Any, Optional +from typing import Any from marshmallow import EXCLUDE, Schema, fields, missing, post_load @@ -13,8 +13,8 @@ def __init__( message: str, detection_details: dict, detection_rule_id: str, - severity: Optional[str] = None, - id: Optional[str] = None, + severity: str | None = None, + id: str | None = None, ) -> None: super().__init__() self.id = id @@ -62,7 +62,7 @@ def build_dto(self, data: dict[str, Any], **_) -> Detection: class DetectionsPerFile(Schema): - def __init__(self, file_name: str, detections: list[Detection], commit_id: Optional[str] = None) -> None: + def __init__(self, file_name: str, detections: list[Detection], commit_id: str | None = None) -> None: super().__init__() self.file_name = file_name self.detections = detections @@ -74,10 +74,10 @@ def __init__( self, did_detect: bool, detections_per_file: list[DetectionsPerFile], - report_url: Optional[str] = None, - scan_id: Optional[str] = None, - err: Optional[str] = None, - verdict: Optional[str] = None, + report_url: str | None = None, + scan_id: str | None = None, + err: str | None = None, + verdict: str | None = None, ) -> None: super().__init__() self.did_detect = did_detect @@ -92,9 +92,9 @@ class ScanResult(Schema): def __init__( self, did_detect: bool, - scan_id: Optional[str] = None, - detections: Optional[list[Detection]] = None, - err: Optional[str] = None, + scan_id: str | None = None, + detections: list[Detection] | None = None, + err: str | None = None, ) -> None: super().__init__() self.did_detect = did_detect @@ -138,7 +138,7 @@ def build_dto(self, data: dict[str, Any], **_) -> 'UploadLinkResponse': class ScanInitializationResponse(Schema): - def __init__(self, scan_id: Optional[str] = None, err: Optional[str] = None) -> None: + def __init__(self, scan_id: str | None = None, err: str | None = None) -> None: super().__init__() self.scan_id = scan_id self.err = err @@ -159,13 +159,13 @@ def build_dto(self, data: dict[str, Any], **_) -> 'ScanInitializationResponse': class ScanDetailsResponse(Schema): def __init__( self, - id: Optional[str] = None, - scan_status: Optional[str] = None, - results_count: Optional[int] = None, - metadata: Optional[str] = None, - message: Optional[str] = None, - scan_update_at: Optional[str] = None, - err: Optional[str] = None, + id: str | None = None, + scan_status: str | None = None, + results_count: int | None = None, + metadata: str | None = None, + message: str | None = None, + scan_update_at: str | None = None, + err: str | None = None, ) -> None: super().__init__() self.id = id @@ -389,9 +389,9 @@ def build_dto(self, data: dict[str, Any], **_) -> SbomReportStorageDetails: class ReportExecution: id: int status: str - error_message: Optional[str] = None - status_message: Optional[str] = None - storage_details: Optional[SbomReportStorageDetails] = None + error_message: str | None = None + status_message: str | None = None + storage_details: SbomReportStorageDetails | None = None class ReportExecutionSchema(Schema): @@ -445,7 +445,7 @@ def build_dto(self, data: dict[str, Any], **_) -> Member: class MemberDetails: items: list[Member] page_size: int - next_page_token: Optional[str] + next_page_token: str | None class RequestedMemberDetailsResultSchema(Schema): @@ -481,11 +481,11 @@ def build_dto(self, data: dict[str, Any], **_) -> ClassificationData: class DetectionRule: classification_data: list[ClassificationData] detection_rule_id: str - custom_remediation_guidelines: Optional[str] = None - remediation_guidelines: Optional[str] = None - description: Optional[str] = None - policy_name: Optional[str] = None - display_name: Optional[str] = None + custom_remediation_guidelines: str | None = None + remediation_guidelines: str | None = None + description: str | None = None + policy_name: str | None = None + display_name: str | None = None class DetectionRuleSchema(Schema): @@ -509,7 +509,7 @@ def build_dto(self, data: dict[str, Any], **_) -> DetectionRule: class ScanResultsSyncFlow: id: str detection_messages: list[dict] - verdict: Optional[str] = None + verdict: str | None = None class ScanResultsSyncFlowSchema(Schema): @@ -594,7 +594,7 @@ def parse(cls, raw_status: object) -> 'McpServerAuthorizationStatus': class McpServerAuthorizationStatusField(fields.Field): - def _serialize(self, value: Optional[McpServerAuthorizationStatus], *_, **__) -> Optional[str]: + def _serialize(self, value: McpServerAuthorizationStatus | None, *_, **__) -> str | None: return value.value if value is not None else None def deserialize(self, value: object, *_, **__) -> McpServerAuthorizationStatus: @@ -603,8 +603,8 @@ def deserialize(self, value: object, *_, **__) -> McpServerAuthorizationStatus: @dataclass class McpServerStatus: - alias: Optional[str] = None - normalized_id: Optional[str] = None + alias: str | None = None + normalized_id: str | None = None status: McpServerAuthorizationStatus = McpServerAuthorizationStatus.UNREVIEWED @@ -624,7 +624,7 @@ def build_dto(self, data: dict[str, Any], **_) -> McpServerStatus: @dataclass class SessionContextResponse: # None when the response carries no (or malformed) MCP server statuses. - mcp_servers: Optional[list[McpServerStatus]] = None + mcp_servers: list[McpServerStatus] | None = None class SessionContextResponseSchema(Schema): diff --git a/cycode/cyclient/report_client.py b/cycode/cyclient/report_client.py index a55b5c40..c207d676 100644 --- a/cycode/cyclient/report_client.py +++ b/cycode/cyclient/report_client.py @@ -1,6 +1,5 @@ import dataclasses import json -from typing import Optional from requests import Response @@ -45,7 +44,7 @@ def __init__(self, client: CycodeClientBase) -> None: self.client = client def request_sbom_report_execution( - self, params: ReportParameters, zip_file: InMemoryZip = None, repository_url: Optional[str] = None + self, params: ReportParameters, zip_file: InMemoryZip = None, repository_url: str | None = None ) -> models.ReportExecution: report_type = 'zipped-file' if zip_file else 'repository-url' url_path = f'{self.SERVICE_NAME}/{self.CREATE_SBOM_REPORT_REQUEST_PATH}'.format(report_type=report_type) diff --git a/cycode/cyclient/scan_client.py b/cycode/cyclient/scan_client.py index 19e79ddb..ea9a3734 100644 --- a/cycode/cyclient/scan_client.py +++ b/cycode/cyclient/scan_client.py @@ -1,6 +1,7 @@ import json +from collections.abc import Callable from copy import deepcopy -from typing import TYPE_CHECKING, Callable, Optional, Union +from typing import TYPE_CHECKING from uuid import UUID import requests @@ -119,7 +120,7 @@ def zipped_file_scan_async( scan_parameters: dict, is_git_diff: bool = False, is_commit_range: bool = False, - on_upload_progress: Optional[Callable[[int, int], None]] = None, + on_upload_progress: Callable[[int, int], None] | None = None, ) -> models.ScanInitializationResponse: response = self.scan_cycode_client.post_multipart( url_path=self.get_zipped_file_scan_async_url_path(scan_type), @@ -145,7 +146,7 @@ def upload_to_presigned_post( url: str, fields: dict[str, str], zip_file: 'InMemoryZip', - on_upload_progress: Optional[Callable[[int, int], None]] = None, + on_upload_progress: Callable[[int, int], None] | None = None, ) -> None: all_files = {key: (None, value) for key, value in fields.items()} all_files['file'] = ('multiple_files_scan.zip', zip_file.read(), 'application/octet-stream') @@ -315,7 +316,7 @@ def _get_policy_type_by_scan_type(scan_type: str) -> str: def parse_detection_rules_response(response: Response) -> list[models.DetectionRule]: return models.DetectionRuleSchema().load(response.json(), many=True) - def get_detection_rules(self, detection_rules_ids: Union[set[str], list[str]]) -> list[models.DetectionRule]: + def get_detection_rules(self, detection_rules_ids: set[str] | list[str]) -> list[models.DetectionRule]: response = self.scan_cycode_client.get( url_path=self.get_detection_rules_path(), params={'ids': detection_rules_ids}, @@ -372,7 +373,7 @@ def get_scan_configuration_path(self, scan_type: str) -> str: correct_scan_type = self.scan_config.get_async_scan_type(scan_type) return f'{self.get_scan_service_url_path(scan_type)}/{correct_scan_type}/configuration' - def get_scan_configuration(self, scan_type: str, remote_url: Optional[str] = None) -> models.ScanConfiguration: + def get_scan_configuration(self, scan_type: str, remote_url: str | None = None) -> models.ScanConfiguration: params = {} if remote_url: params['remote_url'] = remote_url @@ -385,8 +386,8 @@ def get_scan_configuration(self, scan_type: str, remote_url: Optional[str] = Non return models.ScanConfigurationSchema().load(response.json()) def get_scan_configuration_safe( - self, scan_type: str, remote_url: Optional[str] = None - ) -> Optional['models.ScanConfiguration']: + self, scan_type: str, remote_url: str | None = None + ) -> 'models.ScanConfiguration | None': try: return self.get_scan_configuration(scan_type, remote_url) except RequestHttpError as e: diff --git a/cycode/logger.py b/cycode/logger.py index c5cdebcf..990c7021 100644 --- a/cycode/logger.py +++ b/cycode/logger.py @@ -1,6 +1,6 @@ import logging import sys -from typing import ClassVar, NamedTuple, Optional, Union +from typing import ClassVar, NamedTuple import click import typer @@ -42,10 +42,10 @@ class CreatedLogger(NamedTuple): class LoggersManager: loggers: ClassVar[set[CreatedLogger]] = set() - global_logging_level: Optional[int] = None + global_logging_level: int | None = None -def get_logger_level() -> Optional[Union[int, str]]: +def get_logger_level() -> int | str | None: if LoggersManager.global_logging_level is not None: return LoggersManager.global_logging_level @@ -53,7 +53,7 @@ def get_logger_level() -> Optional[Union[int, str]]: return logging.getLevelName(config_level) -def get_logger(logger_name: Optional[str] = None, control_level_in_runtime: bool = True) -> logging.Logger: +def get_logger(logger_name: str | None = None, control_level_in_runtime: bool = True) -> logging.Logger: new_logger = logging.getLogger(logger_name) new_logger.setLevel(get_logger_level()) diff --git a/poetry.lock b/poetry.lock index d99dd210..d646ce5d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "altgraph" @@ -13,19 +13,6 @@ files = [ {file = "altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7"}, ] -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version < \"3.14\"" -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - [[package]] name = "annotated-types" version = "0.8.0" @@ -33,7 +20,6 @@ description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.14\"" files = [ {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, @@ -41,23 +27,23 @@ files = [ [[package]] name = "anyio" -version = "4.12.1" +version = "4.15.1" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, - {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, + {file = "anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101"}, + {file = "anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} +typing_extensions = {version = ">=4.16.0", markers = "python_version < \"3.15\""} [package.extras] -trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] +trio = ["trio (>=0.32.0)"] [[package]] name = "arrow" @@ -86,7 +72,6 @@ description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, @@ -99,7 +84,7 @@ description = "Backport of Python 3.11's datetime.fromisoformat" optional = false python-versions = ">3" groups = ["main"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4"}, {file = "backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c"}, @@ -169,7 +154,7 @@ description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\" and platform_python_implementation != \"PyPy\"" +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0"}, {file = "cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd"}, @@ -526,59 +511,58 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "49.0.0" +version = "50.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.9" groups = ["main"] -markers = "python_version >= \"3.10\"" -files = [ - {file = "cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9"}, - {file = "cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f"}, - {file = "cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459"}, - {file = "cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e"}, - {file = "cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8"}, - {file = "cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3"}, - {file = "cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27"}, - {file = "cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61"}, - {file = "cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36"}, - {file = "cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e"}, - {file = "cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b"}, - {file = "cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6"}, - {file = "cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493"}, +files = [ + {file = "cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a"}, + {file = "cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959"}, + {file = "cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b"}, + {file = "cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648"}, + {file = "cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3"}, + {file = "cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6"}, + {file = "cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149"}, + {file = "cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf"}, + {file = "cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239"}, + {file = "cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558"}, + {file = "cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e"}, + {file = "cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b"}, + {file = "cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20"}, ] [package.dependencies] @@ -610,7 +594,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["main", "test"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, @@ -651,7 +635,6 @@ files = [ [package.dependencies] gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.10.0.2", markers = "python_version < \"3.10\""} [package.extras] doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] @@ -664,7 +647,6 @@ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -677,7 +659,6 @@ description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -700,7 +681,6 @@ description = "The next generation HTTP client." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -726,7 +706,6 @@ description = "Consume Server-Sent Event (SSE) messages with HTTPX." optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc"}, {file = "httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d"}, @@ -747,44 +726,6 @@ files = [ [package.extras] all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["executable"] -markers = "python_version == \"3.9\"" -files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -groups = ["test"] -markers = "python_version < \"3.14\"" -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -792,7 +733,6 @@ description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" groups = ["test"] -markers = "python_version >= \"3.14\"" files = [ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, @@ -805,7 +745,6 @@ description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, @@ -828,7 +767,6 @@ description = "The JSON Schema meta-schemas and vocabularies, exposed as a Regis optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, @@ -853,32 +791,6 @@ files = [ [package.dependencies] altgraph = ">=0.17" -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version < \"3.14\"" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - [[package]] name = "markdown-it-py" version = "4.2.0" @@ -886,7 +798,6 @@ description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.14\"" files = [ {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, @@ -906,25 +817,20 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-tim [[package]] name = "marshmallow" -version = "4.0.1" +version = "4.3.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "marshmallow-4.0.1-py3-none-any.whl", hash = "sha256:72f14ef346f81269dbddee891bac547dda1501e9e08b6a809756ea3dbb7936a1"}, - {file = "marshmallow-4.0.1.tar.gz", hash = "sha256:e1d860bd262737cb2d34e1541b84cb52c32c72c9474e3fe6f30f137ef8b0d97f"}, + {file = "marshmallow-4.3.1-py3-none-any.whl", hash = "sha256:e65accfbe277546df92ed7996a678c90e063e9a7c2a2f5e03f7d0b90e3768c42"}, + {file = "marshmallow-4.3.1.tar.gz", hash = "sha256:fb6b8048af08d4ab061610d5b7d3696a7e4c95337dbda880edb9f95812cabc20"}, ] [package.dependencies] backports-datetime-fromisoformat = {version = "*", markers = "python_version < \"3.11\""} typing-extensions = {version = "*", markers = "python_version < \"3.11\""} -[package.extras] -dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] -docs = ["autodocsumm (==0.2.14)", "furo (==2025.7.19)", "sphinx (==8.2.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.1)", "sphinxext-opengraph (==0.12.0)"] -tests = ["pytest", "simplejson"] - [[package]] name = "mcp" version = "1.28.1" @@ -932,7 +838,6 @@ description = "Model Context Protocol SDK" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df"}, {file = "mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683"}, @@ -1074,7 +979,7 @@ description = "C parser in Python" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\" and platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" files = [ {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, @@ -1242,7 +1147,6 @@ description = "Settings management using Pydantic" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440"}, {file = "pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f"}, @@ -1312,7 +1216,6 @@ files = [ [package.dependencies] altgraph = "*" -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} packaging = ">=22.0" pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} @@ -1338,19 +1241,18 @@ files = [ ] [package.dependencies] -importlib_metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} packaging = ">=22.0" [[package]] name = "pyjwt" -version = "2.13.0" +version = "2.15.1" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728"}, - {file = "pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423"}, + {file = "pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193"}, + {file = "pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8"}, ] [package.dependencies] @@ -1362,21 +1264,21 @@ crypto = ["cryptography (>=3.4.0)"] [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.3" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["test"] files = [ - {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, - {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, + {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, + {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, ] [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1" -packaging = ">=20" +iniconfig = ">=1.0.1" +packaging = ">=22" pluggy = ">=1.5,<2" pygments = ">=2.7.2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} @@ -1424,7 +1326,6 @@ description = "Read key-value pairs from a .env file and set them as environment optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, @@ -1440,7 +1341,6 @@ description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23"}, {file = "python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e"}, @@ -1453,7 +1353,7 @@ description = "Python for Windows Extensions" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "sys_platform == \"win32\" and python_version >= \"3.10\"" +markers = "sys_platform == \"win32\"" files = [ {file = "pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e"}, {file = "pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db"}, @@ -1581,7 +1481,6 @@ description = "JSON Referencing + Python" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, @@ -1594,36 +1493,36 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "requests" -version = "2.32.5" +version = "2.34.2" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "test"] files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] -certifi = ">=2017.4.17" +certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "responses" -version = "0.26.2" +version = "0.26.3" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" groups = ["test"] files = [ - {file = "responses-0.26.2-py3-none-any.whl", hash = "sha256:6fdfeabd58e5ec473b98dfe02e6d46d3173bd8dd573eff2ccccf1a05a5135364"}, - {file = "responses-0.26.2.tar.gz", hash = "sha256:9c9259b46a8349197edebf43cfa68a87e1a2802ef503ff8b2fecbabc0b45afd8"}, + {file = "responses-0.26.3-py3-none-any.whl", hash = "sha256:74474f799334ac4f37d93b6437ecc3bb1bb5c77a8d31780a338643be2dce0af8"}, + {file = "responses-0.26.3.tar.gz", hash = "sha256:b0c11ca8131b8b227b8d5108e6ed39772222bd5aab030ed430e8f99057c4c409"}, ] [package.dependencies] @@ -1660,7 +1559,7 @@ description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version < \"3.14\" and python_version >= \"3.10\"" +markers = "python_version < \"3.14\"" files = [ {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, @@ -1908,54 +1807,32 @@ files = [ [[package]] name = "ruff" -version = "0.15.20" +version = "0.16.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078"}, - {file = "ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b"}, - {file = "ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b"}, - {file = "ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487"}, - {file = "ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3"}, - {file = "ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053"}, - {file = "ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4"}, - {file = "ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460"}, - {file = "ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21"}, - {file = "ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415"}, - {file = "ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca"}, - {file = "ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566"}, + {file = "ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96"}, + {file = "ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67"}, + {file = "ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588"}, + {file = "ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385"}, + {file = "ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b"}, + {file = "ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284"}, + {file = "ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161"}, ] -[[package]] -name = "setuptools" -version = "82.0.1" -description = "Most extensible Python build backend with support for C/C++ extension modules" -optional = false -python-versions = ">=3.9" -groups = ["executable"] -markers = "python_version < \"3.14\"" -files = [ - {file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"}, - {file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] - [[package]] name = "setuptools" version = "83.0.0" @@ -1963,7 +1840,7 @@ description = "Most extensible Python build backend with support for C/C++ exten optional = false python-versions = ">=3.10" groups = ["executable"] -markers = "python_version == \"3.14\"" +markers = "python_version < \"3.15\"" files = [ {file = "setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"}, {file = "setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef"}, @@ -2021,7 +1898,6 @@ description = "SSE plugin for Starlette" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "sse_starlette-3.4.6-py3-none-any.whl", hash = "sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6"}, {file = "sse_starlette-3.4.6.tar.gz", hash = "sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627"}, @@ -2045,7 +1921,6 @@ description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6"}, {file = "starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0"}, @@ -2081,7 +1956,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["main", "test"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, @@ -2151,7 +2026,6 @@ description = "Verify certificates using native system trust stores" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\"" files = [ {file = "truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981"}, {file = "truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301"}, @@ -2186,7 +2060,7 @@ files = [ {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] -markers = {test = "python_version < \"3.11\""} +markers = {test = "python_version == \"3.10\""} [[package]] name = "typing-inspection" @@ -2217,14 +2091,14 @@ files = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.8.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "test"] files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, + {file = "urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3"}, + {file = "urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63"}, ] [package.extras] @@ -2240,7 +2114,7 @@ description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\" and sys_platform != \"emscripten\"" +markers = "sys_platform != \"emscripten\"" files = [ {file = "uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b"}, {file = "uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0"}, @@ -2254,28 +2128,7 @@ typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] standard = ["httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=13.0)"] -[[package]] -name = "zipp" -version = "3.23.1" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["executable"] -markers = "python_version == \"3.9\"" -files = [ - {file = "zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc"}, - {file = "zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [metadata] lock-version = "2.1" -python-versions = ">=3.9" -content-hash = "44bcd4aef8c17a3e700a307521aa45adbb8422ea9a35a1647373d73043c37deb" +python-versions = ">=3.10" +content-hash = "2557a6bcdafca75576b98129fc399870f949120299981e590a99d0f33ed5d049" diff --git a/process_executable_file.py b/process_executable_file.py index 19cfbb44..f51a4fd5 100755 --- a/process_executable_file.py +++ b/process_executable_file.py @@ -15,7 +15,6 @@ import shutil from pathlib import Path from string import Template -from typing import Union _ARCHIVE_FORMAT = 'zip' _HASH_FILE_EXT = '.sha256' @@ -30,7 +29,7 @@ DirHashes = list[tuple[str, str]] -def get_hash_of_file(file_path: Union[str, Path]) -> str: +def get_hash_of_file(file_path: str | Path) -> str: with open(file_path, 'rb') as f: return hashlib.sha256(f.read()).hexdigest() @@ -85,7 +84,7 @@ def get_os_name() -> str: def get_cli_file_name(suffix: str = '', ext: str = '') -> str: os_name = get_os_name() if os_name not in _OS_TO_CLI_DIST_TEMPLATE: - raise Exception(f'Unsupported OS: {os_name}') + raise Exception(f'Unsupported OS: {os_name}') # noqa: TRY002 return _OS_TO_CLI_DIST_TEMPLATE[os_name].substitute(suffix=suffix, ext=ext) diff --git a/pyinstaller.spec b/pyinstaller.spec index eb65d9cd..3e291924 100644 --- a/pyinstaller.spec +++ b/pyinstaller.spec @@ -6,7 +6,6 @@ import os import platform import re import subprocess -import sys _IS_WINDOWS = platform.system() == 'Windows' @@ -47,8 +46,7 @@ _hiddenimports = [ # truststore is imported lazily inside cycode/cli/utils/trust_store.py, and it picks its platform # backend behind a sys.platform branch. Only the current platform's backend actually resolves. -if sys.version_info >= (3, 10): - _hiddenimports += ['truststore', 'truststore._windows', 'truststore._macos', 'truststore._openssl'] +_hiddenimports += ['truststore', 'truststore._windows', 'truststore._macos', 'truststore._openssl'] def _build_windows_version_info(version: str): diff --git a/pyproject.toml b/pyproject.toml index bbabcd87..c3a874e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ description = "Boost security in your dev lifecycle via SAST, SCA, Secrets & IaC keywords = ["secret-scan", "cycode", "devops", "token", "secret", "security", "code"] authors = [{name = "Cycode", email = "support@cycode.com"}] license = "MIT" -requires-python = ">=3.9" +requires-python = ">=3.10" readme = "README.md" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -13,7 +13,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -36,41 +35,40 @@ version = "0.0.0" # DON'T TOUCH. Placeholder. Will be filled automatically on po click = ">=8.1.0,<8.2.0" colorama = ">=0.4.3,<0.5.0" pyyaml = ">=6.0,<7.0" -# capped while we support Python 3.9 -marshmallow = ">=4.0.1,<4.1.0" +marshmallow = ">=4.1.2,<5.0.0" # floor required by get_staged_diff_index() gitpython = ">=3.1.62,<3.2.0" arrow = ">=1.0.0,<1.5.0" -requests = ">=2.32.4,<3.0" -urllib3 = ">=2.4.0,<3.0.0" -pyjwt = ">=2.8.0,<3.0" +requests = ">=2.33.0,<3.0" +urllib3 = ">=2.8.0,<3.0.0" +pyjwt = ">=2.15.0,<3.0" rich = ">=15.0.0,<16.0.0" patch-ng = "1.19.1" typer = "^0.15.3" tenacity = ">=9.1.2,<9.2.0" -mcp = { version = ">=1.28.1,<2.0.0", markers = "python_version >= '3.10'" } -truststore = { version = ">=0.10.4,<0.11.0", markers = "python_version >= '3.10'" } -pywin32 = { version = ">=312,<313", markers = "python_version >= '3.10' and sys_platform == 'win32'"} +mcp = ">=1.28.1,<2.0.0" +truststore = ">=0.10.4,<0.11.0" +pywin32 = { version = ">=312,<313", markers = "sys_platform == 'win32'" } pydantic = ">=2.11.5,<3.0.0" pathvalidate = ">=3.3.1,<4.0.0" tomli-w = ">=1.0.0,<2.0.0" tomli = {version = ">=2.0.0,<3.0.0", python = "<3.11"} -anyio = ">=4.0.0, <4.13.0" +anyio = ">=4.14.2,<5.0.0" [tool.poetry.group.test.dependencies] mock = ">=5.2.0,<5.3.0" -pytest = ">=7.3.1,<8.5.0" +pytest = ">=9.0.3,<9.1.0" pytest-mock = ">=3.15.1,<3.16.0" coverage = ">=7.2.3,<7.11.0" -responses = ">=0.23.1,<0.27.0" +responses = ">=0.26.3,<0.27.0" pyfakefs = ">=5.7.2,<5.11.0" [tool.poetry.group.executable.dependencies] -pyinstaller = {version=">=6.22.2,<7.0.0", python=">=3.9,<3.15"} +pyinstaller = {version=">=6.22.2,<7.0.0", python=">=3.10,<3.15"} dunamai = ">=1.26.2,<1.27.0" [tool.poetry.group.dev.dependencies] -ruff = "0.15.20" +ruff = "0.16.9" [tool.pytest.ini_options] log_cli = true @@ -87,7 +85,7 @@ style = "pep440" [tool.ruff] line-length = 120 -target-version = "py39" +target-version = "py310" [tool.ruff.lint] extend-select = [ diff --git a/tests/cli/apps/mcp/test_mcp_command.py b/tests/cli/apps/mcp/test_mcp_command.py index cbb65b1c..d6d1f81e 100644 --- a/tests/cli/apps/mcp/test_mcp_command.py +++ b/tests/cli/apps/mcp/test_mcp_command.py @@ -1,13 +1,9 @@ import json import os -import sys from unittest.mock import AsyncMock, patch import pytest -if sys.version_info < (3, 10): - pytest.skip('MCP requires Python 3.10+', allow_module_level=True) - from cycode.cli.apps.mcp.mcp_command import ( _build_scan_summary, _sanitize_file_path, diff --git a/tests/cli/commands/ai_guardrails/ides/test_codex.py b/tests/cli/commands/ai_guardrails/ides/test_codex.py index 655c75ee..b867de39 100644 --- a/tests/cli/commands/ai_guardrails/ides/test_codex.py +++ b/tests/cli/commands/ai_guardrails/ides/test_codex.py @@ -324,7 +324,8 @@ def _make_jwt(claims: dict) -> str: """Build a JWT-shaped token with the given claims (signature ignored).""" header = base64.urlsafe_b64encode(b'{"alg":"RS256"}').rstrip(b'=').decode() payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).rstrip(b'=').decode() - return f'{header}.{payload}.signature-not-verified' + signature = base64.urlsafe_b64encode(b'signature-not-verified').rstrip(b'=').decode() + return f'{header}.{payload}.{signature}' def test_email_from_auth_returns_email(fs: FakeFilesystem, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/cli/commands/ai_guardrails/ides/test_copilot.py b/tests/cli/commands/ai_guardrails/ides/test_copilot.py index be2053bb..eed1b8a6 100644 --- a/tests/cli/commands/ai_guardrails/ides/test_copilot.py +++ b/tests/cli/commands/ai_guardrails/ides/test_copilot.py @@ -7,7 +7,6 @@ import json import os from pathlib import Path -from typing import Optional from pyfakefs.fake_filesystem import FakeFilesystem from pytest_mock import MockerFixture @@ -398,7 +397,7 @@ def _create_plugin_on_disk( fs: FakeFilesystem, plugin_dir: Path, manifest_location: str = '.github/plugin/plugin.json', - manifest_extra: Optional[dict] = None, + manifest_extra: dict | None = None, mcp_file: str = '.mcp.json', server_name: str = 'dummy-server', ) -> None: diff --git a/tests/cli/commands/ai_guardrails/scan/conftest.py b/tests/cli/commands/ai_guardrails/scan/conftest.py index a253fbb5..4e5d85b6 100644 --- a/tests/cli/commands/ai_guardrails/scan/conftest.py +++ b/tests/cli/commands/ai_guardrails/scan/conftest.py @@ -1,5 +1,4 @@ import time -from typing import Optional from cycode.cli.apps.ai_guardrails.scan.guardrail_config import GuardrailConfig @@ -9,8 +8,8 @@ def resolved_guardrails_payload( file_read: str = 'Report', sensitive_path: str = 'Report', mcp: str = 'Report', - globs: Optional[list] = None, - mcp_server: Optional[str] = None, + globs: list | None = None, + mcp_server: str | None = None, ) -> dict: """A platform resolved-config payload with the given per-guardrail modes for the cursor agent.""" payload = { @@ -39,7 +38,7 @@ def resolved_guardrails_payload( return payload -def platform_config(fetched_at: Optional[float] = None, **modes: Optional[str]) -> GuardrailConfig: +def platform_config(fetched_at: float | None = None, **modes: str | None) -> GuardrailConfig: """A cached platform config; keyword args are the per-guardrail modes (see resolved_guardrails_payload).""" return GuardrailConfig( payload=resolved_guardrails_payload(**modes), diff --git a/tests/cli/commands/ai_guardrails/scan/test_handlers.py b/tests/cli/commands/ai_guardrails/scan/test_handlers.py index 4c5abe24..02eca03e 100644 --- a/tests/cli/commands/ai_guardrails/scan/test_handlers.py +++ b/tests/cli/commands/ai_guardrails/scan/test_handlers.py @@ -4,7 +4,7 @@ import threading import time from multiprocessing import synchronize -from typing import Any, Optional +from typing import Any from unittest.mock import MagicMock, patch import pytest @@ -592,7 +592,7 @@ def _server_check_policy(default_policy: dict[str, Any], server_action: str = 'b return default_policy -def _mcp_payload(server: Optional[str] = 'github') -> AIHookPayload: +def _mcp_payload(server: str | None = 'github') -> AIHookPayload: return AIHookPayload( event_name='McpExecution', conversation_id='conv-1', @@ -608,7 +608,7 @@ def _cached_statuses(*rows: tuple[str, str]) -> McpServerStatuses: return McpServerStatuses(servers=servers, fetched_at=time.time()) -def _reported_event(mock_ctx: MagicMock) -> tuple[AIHookOutcome, Optional[BlockReason]]: +def _reported_event(mock_ctx: MagicMock) -> tuple[AIHookOutcome, BlockReason | None]: call_args = mock_ctx.obj['ai_security_client'].create_event.call_args return call_args.args[2], call_args.kwargs['block_reason'] @@ -683,7 +683,7 @@ def test_unauthorized_mcp_server_enforcement( mock_ctx: MagicMock, default_policy: dict[str, Any], rows: tuple, - server: Optional[str], + server: str | None, expected_action: DecisionAction, ) -> None: mock_statuses.return_value = _cached_statuses(*rows) diff --git a/tests/cli/commands/ai_guardrails/scan/test_mcp_server_status.py b/tests/cli/commands/ai_guardrails/scan/test_mcp_server_status.py index b7842384..09f7fb68 100644 --- a/tests/cli/commands/ai_guardrails/scan/test_mcp_server_status.py +++ b/tests/cli/commands/ai_guardrails/scan/test_mcp_server_status.py @@ -3,7 +3,6 @@ import json import time from pathlib import Path -from typing import Optional import pytest from pyfakefs.fake_filesystem import FakeFilesystem @@ -52,7 +51,7 @@ def test_cache_file_sits_next_to_the_guardrail_config() -> None: ], ) def test_parse_status_is_case_insensitive_and_unknown_reads_unreviewed( - raw: Optional[str], expected: McpServerAuthorizationStatus + raw: str | None, expected: McpServerAuthorizationStatus ) -> None: assert McpServerAuthorizationStatus.parse(raw) == expected @@ -66,7 +65,7 @@ def test_parse_status_is_case_insensitive_and_unknown_reads_unreviewed( (None, False), ], ) -def test_is_enforced(status: Optional[McpServerAuthorizationStatus], expected: bool) -> None: +def test_is_enforced(status: McpServerAuthorizationStatus | None, expected: bool) -> None: assert is_enforced(status) is expected diff --git a/tests/cli/commands/ai_guardrails/scan/test_policy.py b/tests/cli/commands/ai_guardrails/scan/test_policy.py index cf07a8b3..4cf80dea 100644 --- a/tests/cli/commands/ai_guardrails/scan/test_policy.py +++ b/tests/cli/commands/ai_guardrails/scan/test_policy.py @@ -1,7 +1,6 @@ """Tests for AI guardrails policy loading and management.""" from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -156,7 +155,7 @@ def test_load_policy_with_repo_config(mock_load: MagicMock) -> None: repo_path = Path('/fake/repo') repo_config = repo_path / '.cycode' / 'ai-guardrails.yaml' - def side_effect(path: Path) -> Optional[dict]: + def side_effect(path: Path) -> dict | None: if path == repo_config: return { 'mode': 'block', diff --git a/tests/cli/commands/ai_guardrails/scan/test_scan_command.py b/tests/cli/commands/ai_guardrails/scan/test_scan_command.py index b756d534..f6afccb6 100644 --- a/tests/cli/commands/ai_guardrails/scan/test_scan_command.py +++ b/tests/cli/commands/ai_guardrails/scan/test_scan_command.py @@ -3,7 +3,6 @@ import json import time from io import StringIO -from typing import Optional from unittest.mock import MagicMock import pytest @@ -334,8 +333,8 @@ def _run_prompt_scan( self, mock_ctx: MagicMock, mocker: MockerFixture, - config: Optional[GuardrailConfig], - policy: Optional[dict] = None, + config: GuardrailConfig | None, + policy: dict | None = None, ) -> str: payload = json.dumps({'hook_event_name': 'beforeSubmitPrompt', 'conversation_id': 'c-1', 'prompt': 'test'}) mocker.patch('sys.stdin', StringIO(payload)) @@ -513,7 +512,7 @@ def test_unauthorized_mcp_server_block_keeps_the_mcp_event_synchronous( mocker: MockerFixture, mock_respawn: MagicMock, not_detached: None, - mcp_server: Optional[str], + mcp_server: str | None, expect_detach: bool, ) -> None: config = platform_config(mcp='report', mcp_server=mcp_server) diff --git a/tests/cli/commands/ai_guardrails/test_session_start_command.py b/tests/cli/commands/ai_guardrails/test_session_start_command.py index bd63dfef..7452a892 100644 --- a/tests/cli/commands/ai_guardrails/test_session_start_command.py +++ b/tests/cli/commands/ai_guardrails/test_session_start_command.py @@ -4,7 +4,6 @@ import time from io import StringIO from pathlib import Path -from typing import Optional from unittest.mock import ANY, MagicMock, patch import pytest @@ -736,7 +735,7 @@ def _run_with_config( mock_ctx: MagicMock, monkeypatch: pytest.MonkeyPatch, ai_client: MagicMock, - config: Optional[GuardrailConfig], + config: GuardrailConfig | None, tenant_id: str = 'tenant-a', ) -> None: monkeypatch.setattr(_session_start_mod, 'load_guardrail_config', MagicMock(return_value=config)) @@ -760,7 +759,7 @@ def _age_report_cache(seconds: float) -> None: cache_path.write_text(json.dumps(cache), encoding='utf-8') -def _statuses_client(mcp_servers: Optional[list] = _SERVERS) -> MagicMock: +def _statuses_client(mcp_servers: list | None = _SERVERS) -> MagicMock: ai_client = MagicMock() ai_client.report_session_context.return_value = SessionContextResponse(mcp_servers=mcp_servers) return ai_client @@ -779,7 +778,7 @@ def test_session_context_response_saves_the_mcp_server_statuses( @pytest.mark.parametrize('response', [None, SessionContextResponse(mcp_servers=None)]) def test_failed_or_malformed_session_context_response_keeps_the_cache( - response: Optional[SessionContextResponse], + response: SessionContextResponse | None, mock_ctx: MagicMock, monkeypatch: pytest.MonkeyPatch, mock_save_mcp_server_statuses: MagicMock, @@ -802,7 +801,7 @@ def test_failed_or_malformed_session_context_response_keeps_the_cache( ], ) def test_unchanged_context_is_skipped_for_the_report_ttl_while_the_guardrail_is_off( - config: Optional[GuardrailConfig], mock_ctx: MagicMock, monkeypatch: pytest.MonkeyPatch + config: GuardrailConfig | None, mock_ctx: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: ai_client = _statuses_client() diff --git a/tests/cli/commands/version/test_version_checker.py b/tests/cli/commands/version/test_version_checker.py index 14d6150e..31002d9c 100644 --- a/tests/cli/commands/version/test_version_checker.py +++ b/tests/cli/commands/version/test_version_checker.py @@ -1,5 +1,5 @@ import time -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from unittest.mock import MagicMock, patch import pytest @@ -95,7 +95,7 @@ def test_check_for_update_scenarios( version_checker_cached: 'VersionChecker', current_version: str, latest_version: str, - expected_result: Optional[str], + expected_result: str | None, ) -> None: with patch.multiple( version_checker_cached, diff --git a/tests/cli/files_collector/sca/go/test_restore_go_dependencies.py b/tests/cli/files_collector/sca/go/test_restore_go_dependencies.py index 633d24e8..90f6bd6f 100644 --- a/tests/cli/files_collector/sca/go/test_restore_go_dependencies.py +++ b/tests/cli/files_collector/sca/go/test_restore_go_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -62,8 +61,8 @@ def test_generated_output_file_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: # Go uses create_output_file_manually=True; output_file_path is provided target = output_file_path or str(output_path) diff --git a/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py b/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py index 43d34e30..cb7520af 100644 --- a/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py +++ b/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -120,8 +119,8 @@ def test_generated_dep_tree_file_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: # Gradle uses create_output_file_manually=True; output_file_path is provided target = output_file_path or str(output_path) @@ -164,8 +163,8 @@ def test_kts_build_file_also_cleaned_up(self, restore_gradle: RestoreGradleDepen def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: target = output_file_path or str(output_path) Path(target).write_text('compileClasspath\n') diff --git a/tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py b/tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py index f365bd92..f682b40b 100644 --- a/tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py +++ b/tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -63,8 +62,8 @@ def test_generated_bom_is_deleted_after_primary_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: bom_path.write_text('{"bomFormat": "CycloneDX", "components": []}') return 'output' @@ -92,8 +91,8 @@ def test_generated_dep_tree_is_deleted_after_secondary_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: dep_tree_path.write_text('[INFO] com.example:my-app:jar:1.0.0\n') return '[INFO] BUILD SUCCESS' diff --git a/tests/cli/files_collector/sca/npm/test_restore_bun_dependencies.py b/tests/cli/files_collector/sca/npm/test_restore_bun_dependencies.py index e9a64c3c..8ff52591 100644 --- a/tests/cli/files_collector/sca/npm/test_restore_bun_dependencies.py +++ b/tests/cli/files_collector/sca/npm/test_restore_bun_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -177,8 +176,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('{"lockfileVersion": 1}\n') return 'output' diff --git a/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py b/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py index 821a7fb1..83228e9f 100644 --- a/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py +++ b/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py @@ -1,6 +1,5 @@ import json from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -150,8 +149,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('{"lockfileVersion": 3}') return 'output' @@ -213,7 +212,7 @@ def _member_document(member_dir: Path) -> Document: return Document(str(manifest), manifest.read_text(), absolute_path=str(manifest)) @staticmethod - def _write_workspace_root(root: Path, *, lockfile_members: Optional[list] = None) -> None: + def _write_workspace_root(root: Path, *, lockfile_members: list | None = None) -> None: (root / 'package.json').write_text('{"name": "root", "workspaces": ["frontend"]}') if lockfile_members is None: return diff --git a/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py b/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py index 5f6d1f17..d9ff9629 100644 --- a/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py +++ b/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -108,8 +107,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('lockfileVersion: 5.4\n') return 'output' diff --git a/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py b/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py index 0b1856bd..6df0993a 100644 --- a/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py +++ b/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -108,8 +107,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('# yarn lockfile v1\n') return 'output' diff --git a/tests/cli/files_collector/sca/npm/test_workspace.py b/tests/cli/files_collector/sca/npm/test_workspace.py index 3f0495e2..e9e00250 100644 --- a/tests/cli/files_collector/sca/npm/test_workspace.py +++ b/tests/cli/files_collector/sca/npm/test_workspace.py @@ -3,7 +3,6 @@ import logging import time from pathlib import Path -from typing import Optional from unittest.mock import MagicMock import pytest @@ -903,7 +902,7 @@ def test_a_real_top_level_key_still_ends_the_block(self, tmp_path: Path) -> None ('root importer only', "lockfileVersion: '9.0'\nimporters:\n .: {}\n", None), ], ) - def test_lockfile_shapes(self, tmp_path: Path, label: str, text: str, expected: Optional[set]) -> None: + def test_lockfile_shapes(self, tmp_path: Path, label: str, text: str, expected: set | None) -> None: got = self._members(tmp_path, text) assert got == (None if expected is None else frozenset(expected)), label diff --git a/tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py b/tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py index 0ec13441..e00796a1 100644 --- a/tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py +++ b/tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -60,8 +59,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('{"version": 1, "dependencies": {}}') return 'output' diff --git a/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py b/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py index 6e3ea53b..394af68a 100644 --- a/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py +++ b/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -85,8 +84,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('{"_readme": [], "packages": []}') return 'output' diff --git a/tests/cli/files_collector/sca/python/test_restore_pip_dependencies.py b/tests/cli/files_collector/sca/python/test_restore_pip_dependencies.py index c0f17476..e78bd95d 100644 --- a/tests/cli/files_collector/sca/python/test_restore_pip_dependencies.py +++ b/tests/cli/files_collector/sca/python/test_restore_pip_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -134,8 +133,8 @@ def test_pyproject_toml_runs_pip_lock_dot(self, restore_pip: RestorePipDependenc def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: seen_commands.extend(commands) (tmp_path / PIP_LOCK_FILE_NAME).write_text('lock-version = "1.0"\n') @@ -160,8 +159,8 @@ def test_requirements_txt_runs_pip_lock_dash_r(self, restore_pip: RestorePipDepe def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: seen_commands.extend(commands) (tmp_path / PIP_LOCK_FILE_NAME).write_text('lock-version = "1.0"\n') @@ -188,8 +187,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('lock-version = "1.0"\n') return 'output' diff --git a/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py b/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py index a6d97320..11801e7f 100644 --- a/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py +++ b/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -89,8 +88,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('{"_meta": {}, "default": {}, "develop": {}}') return 'output' diff --git a/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py b/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py index cf4c312b..2f25cf5c 100644 --- a/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py +++ b/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -118,8 +117,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('# This file is generated by Poetry\n') return 'output' diff --git a/tests/cli/files_collector/sca/python/test_restore_uv_dependencies.py b/tests/cli/files_collector/sca/python/test_restore_uv_dependencies.py index 70e4e7ae..18530c66 100644 --- a/tests/cli/files_collector/sca/python/test_restore_uv_dependencies.py +++ b/tests/cli/files_collector/sca/python/test_restore_uv_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -109,8 +108,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('version = 1\n') return 'output' diff --git a/tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py b/tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py index ac3e9d73..6b6d1da5 100644 --- a/tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py +++ b/tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -60,8 +59,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('GEM\n remote: https://rubygems.org/\n specs:\n') return 'output' diff --git a/tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py b/tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py index 415e5f94..315e5451 100644 --- a/tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py +++ b/tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -60,8 +59,8 @@ def test_generated_lockfile_is_deleted_after_restore( def side_effect( commands: list, timeout: int, - output_file_path: Optional[str] = None, - working_directory: Optional[str] = None, + output_file_path: str | None = None, + working_directory: str | None = None, ) -> str: lock_path.write_text('[{"org": "org.typelevel", "name": "cats-core", "version": "2.10.0"}]') return 'output' diff --git a/tests/cli/files_collector/sca/test_base_restore_dependencies.py b/tests/cli/files_collector/sca/test_base_restore_dependencies.py index 7d8d8743..1e2b4680 100644 --- a/tests/cli/files_collector/sca/test_base_restore_dependencies.py +++ b/tests/cli/files_collector/sca/test_base_restore_dependencies.py @@ -5,7 +5,6 @@ """ from pathlib import Path -from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -60,7 +59,7 @@ def _make_execute_side_effect(lock_path: Path, content: str = _LOCK_CONTENT) -> """Returns an execute_commands side_effect that writes the lock file.""" def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, timeout: int, output_file_path: str | None = None, working_directory: str | None = None ) -> str: lock_path.write_text(content) return 'output' diff --git a/tests/cli/test_app_argv_peek.py b/tests/cli/test_app_argv_peek.py index bd4c61a8..8bffc2c3 100644 --- a/tests/cli/test_app_argv_peek.py +++ b/tests/cli/test_app_argv_peek.py @@ -10,7 +10,6 @@ against what Click's introspection sees on the built command. """ -from typing import Optional from unittest.mock import patch import click @@ -77,6 +76,6 @@ def test_detect_invocation_finds_subcommand_past_flags(argv: list[str]) -> None: (['cycode', 'version'], ('status', None)), ], ) -def test_detect_invocation_edge_cases(argv: list[str], expected: tuple[Optional[str], Optional[str]]) -> None: +def test_detect_invocation_edge_cases(argv: list[str], expected: tuple[str | None, str | None]) -> None: with patch('sys.argv', argv): assert _detect_invocation() == expected diff --git a/tests/cli/utils/test_host_info.py b/tests/cli/utils/test_host_info.py index 8d405517..90d318db 100644 --- a/tests/cli/utils/test_host_info.py +++ b/tests/cli/utils/test_host_info.py @@ -1,6 +1,5 @@ from pathlib import Path from types import SimpleNamespace -from typing import Optional import pytest @@ -31,8 +30,8 @@ def __init__(self) -> None: def _install_fake_pywin32( monkeypatch: pytest.MonkeyPatch, - serial: Optional[str] = _SERIAL, - get_object_error: Optional[Exception] = None, + serial: str | None = _SERIAL, + get_object_error: Exception | None = None, ) -> _ComCalls: calls = _ComCalls() diff --git a/tests/conftest.py b/tests/conftest.py index 4e86168e..0eb14235 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional import pytest import responses @@ -35,9 +34,7 @@ def scan_client() -> ScanClient: return create_scan_client(_CLIENT_ID, _CLIENT_SECRET, hide_response_log=False) -def create_token_based_client( - client_id: Optional[str] = None, client_secret: Optional[str] = None -) -> CycodeTokenBasedClient: +def create_token_based_client(client_id: str | None = None, client_secret: str | None = None) -> CycodeTokenBasedClient: CredentialsManager.FILE_NAME = 'unit-tests-credentials.yaml' if client_id is None: @@ -48,7 +45,7 @@ def create_token_based_client( return CycodeTokenBasedClient(client_id, client_secret) -def create_oidc_based_client(client_id: Optional[str] = None, id_token: Optional[str] = None) -> CycodeOidcBasedClient: +def create_oidc_based_client(client_id: str | None = None, id_token: str | None = None) -> CycodeOidcBasedClient: CredentialsManager.FILE_NAME = 'unit-tests-credentials.yaml' if client_id is None: diff --git a/tests/cyclient/mocked_responses/import_sbom_client.py b/tests/cyclient/mocked_responses/import_sbom_client.py index 04398ace..05505f05 100644 --- a/tests/cyclient/mocked_responses/import_sbom_client.py +++ b/tests/cyclient/mocked_responses/import_sbom_client.py @@ -1,5 +1,3 @@ -from typing import Optional - import responses from responses import matchers @@ -24,7 +22,7 @@ def get_member_details_url(import_sbom_client: ImportSbomClient) -> str: def get_member_details_response( - url: str, email: str, external_id: Optional[str] = None, status: int = 200 + url: str, email: str, external_id: str | None = None, status: int = 200 ) -> responses.Response: items = [] if external_id: @@ -58,7 +56,7 @@ def mock_member_details_response( responses_module: responses, import_sbom_client: ImportSbomClient, email: str, - external_id: Optional[str] = None, + external_id: str | None = None, status: int = 200, ) -> None: """Mock the member details lookup endpoint""" diff --git a/tests/cyclient/mocked_responses/scan_client.py b/tests/cyclient/mocked_responses/scan_client.py index b2be7ab5..5370659b 100644 --- a/tests/cyclient/mocked_responses/scan_client.py +++ b/tests/cyclient/mocked_responses/scan_client.py @@ -1,6 +1,5 @@ import json from pathlib import Path -from typing import Optional from uuid import UUID, uuid4 import responses @@ -16,7 +15,7 @@ def get_zipped_file_scan_async_url(scan_type: str, scan_client: ScanClient) -> s return f'{api_url}/{service_url}' -def get_zipped_file_scan_async_response(url: str, scan_id: Optional[UUID] = None) -> responses.Response: +def get_zipped_file_scan_async_response(url: str, scan_id: UUID | None = None) -> responses.Response: if not scan_id: scan_id = uuid4() @@ -27,19 +26,19 @@ def get_zipped_file_scan_async_response(url: str, scan_id: Optional[UUID] = None return responses.Response(method=responses.POST, url=url, json=json_response, status=200) -def get_scan_details_url(scan_type: str, scan_id: Optional[UUID], scan_client: ScanClient) -> str: +def get_scan_details_url(scan_type: str, scan_id: UUID | None, scan_client: ScanClient) -> str: api_url = scan_client.scan_cycode_client.api_url service_url = scan_client.get_scan_details_path(scan_type, str(scan_id)) return f'{api_url}/{service_url}' -def get_scan_aggregation_report_url(aggregation_id: Optional[UUID], scan_client: ScanClient, scan_type: str) -> str: +def get_scan_aggregation_report_url(aggregation_id: UUID | None, scan_client: ScanClient, scan_type: str) -> str: api_url = scan_client.scan_cycode_client.api_url service_url = scan_client.get_scan_aggregation_report_url_path(str(aggregation_id), scan_type) return f'{api_url}/{service_url}' -def get_scan_aggregation_report_url_response(url: str, aggregation_id: Optional[UUID] = None) -> responses.Response: +def get_scan_aggregation_report_url_response(url: str, aggregation_id: UUID | None = None) -> responses.Response: if not aggregation_id: aggregation_id = uuid4() json_response = {'report_url': f'https://app.domain/cli-logs-aggregation/{aggregation_id}'} @@ -47,7 +46,7 @@ def get_scan_aggregation_report_url_response(url: str, aggregation_id: Optional[ return responses.Response(method=responses.GET, url=url, json=json_response, status=200) -def get_scan_details_response(url: str, scan_id: Optional[UUID] = None) -> responses.Response: +def get_scan_details_response(url: str, scan_id: UUID | None = None) -> responses.Response: if not scan_id: scan_id = uuid4() @@ -155,7 +154,7 @@ def get_scan_from_upload_id_url(scan_type: str, scan_client: ScanClient) -> str: return f'{api_url}/{service_url}' -def get_scan_from_upload_id_response(url: str, scan_id: Optional[UUID] = None) -> responses.Response: +def get_scan_from_upload_id_response(url: str, scan_id: UUID | None = None) -> responses.Response: if not scan_id: scan_id = uuid4() return responses.Response(method=responses.POST, url=url, json={'scan_id': str(scan_id)}, status=200) diff --git a/tests/test_performance_get_all_files.py b/tests/test_performance_get_all_files.py index b0e8653d..e8a3f7ea 100644 --- a/tests/test_performance_get_all_files.py +++ b/tests/test_performance_get_all_files.py @@ -3,17 +3,16 @@ import os import timeit from pathlib import Path -from typing import Union logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) -def filter_files(paths: list[Union[Path, str]]) -> list[str]: +def filter_files(paths: list[Path | str]) -> list[str]: return [str(path) for path in paths if os.path.isfile(path)] -def get_all_files_glob(path: Union[Path, str]) -> list[str]: +def get_all_files_glob(path: Path | str) -> list[str]: # DOESN'T RETURN HIDDEN FILES. CAN'T BE USED # and doesn't show the best performance if not str(path).endswith(os.sep): diff --git a/tests/user_settings/test_configuration_manager.py b/tests/user_settings/test_configuration_manager.py index 5aa7f6a8..b96a5fb4 100644 --- a/tests/user_settings/test_configuration_manager.py +++ b/tests/user_settings/test_configuration_manager.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from unittest.mock import Mock from cycode.cli.consts import DEFAULT_CYCODE_API_URL @@ -68,9 +68,9 @@ def test_get_base_url_not_configured(mocker: 'MockerFixture') -> None: def _configure_mocks( mocker: 'MockerFixture', - expected_env_var_base_url: Optional[str], - expected_local_config_file_base_url: Optional[str], - expected_global_config_file_base_url: Optional[str], + expected_env_var_base_url: str | None, + expected_local_config_file_base_url: str | None, + expected_global_config_file_base_url: str | None, ) -> ConfigurationManager: mocker.patch.object( ConfigurationManager, 'get_api_url_from_environment_variables', return_value=expected_env_var_base_url diff --git a/tests/utils/test_trust_store.py b/tests/utils/test_trust_store.py index 11f63c38..4b293932 100644 --- a/tests/utils/test_trust_store.py +++ b/tests/utils/test_trust_store.py @@ -40,32 +40,12 @@ def opted_in(monkeypatch: pytest.MonkeyPatch) -> None: _set_enable_env(monkeypatch, '1') -# On Python 3.9 truststore is neither installed nor importable, and install() refuses by design, -# so the tests that assert a successful injection cannot run there. -requires_truststore = pytest.mark.skipif( - sys.version_info < trust_store._MIN_PYTHON_VERSION, - reason='truststore requires Python 3.10+', -) - - -@pytest.mark.skipif( - sys.version_info >= trust_store._MIN_PYTHON_VERSION, - reason='covers the Python 3.9 fallback only', -) -def test_install_declines_on_python_39(mocked_truststore: MagicMock, opted_in: None) -> None: - assert trust_store.install() is False - assert trust_store.is_installed() is False - mocked_truststore.inject_into_ssl.assert_not_called() - - -@requires_truststore def test_install_injects_os_trust_store(mocked_truststore: MagicMock, opted_in: None) -> None: assert trust_store.install() is True assert trust_store.is_installed() is True mocked_truststore.inject_into_ssl.assert_called_once_with() -@requires_truststore def test_install_is_idempotent(mocked_truststore: MagicMock, opted_in: None) -> None: assert trust_store.install() is True assert trust_store.install() is True @@ -77,9 +57,8 @@ def test_install_runs_when_opted_in(value: str, mocked_truststore: MagicMock, mo _set_enable_env(monkeypatch, value) assert trust_store.is_enabled() is True - if trust_store.is_supported(): - assert trust_store.install() is True - mocked_truststore.inject_into_ssl.assert_called_once_with() + assert trust_store.install() is True + mocked_truststore.inject_into_ssl.assert_called_once_with() @pytest.mark.parametrize('value', ['0', 'false', 'no', '']) @@ -101,17 +80,6 @@ def test_install_skipped_when_env_var_absent(mocked_truststore: MagicMock) -> No mocked_truststore.inject_into_ssl.assert_not_called() -def test_install_skipped_on_unsupported_python( - mocked_truststore: MagicMock, monkeypatch: pytest.MonkeyPatch, opted_in: None -) -> None: - monkeypatch.setattr(trust_store.sys, 'version_info', (3, 9, 21)) - - assert trust_store.install() is False - assert trust_store.is_installed() is False - mocked_truststore.inject_into_ssl.assert_not_called() - - -@requires_truststore def test_install_swallows_import_error(monkeypatch: pytest.MonkeyPatch, opted_in: None) -> None: import builtins @@ -128,7 +96,6 @@ def _raising_import(name: str, *args, **kwargs): # noqa: ANN202 assert trust_store.is_installed() is False -@requires_truststore def test_install_swallows_injection_error(mocked_truststore: MagicMock, opted_in: None) -> None: mocked_truststore.inject_into_ssl.side_effect = RuntimeError('no trust store on this machine') @@ -200,9 +167,7 @@ def test_windows_legacy_adapter_skipped_when_trust_store_installed(monkeypatch: def test_windows_legacy_adapter_kept_when_trust_store_inactive(monkeypatch: pytest.MonkeyPatch) -> None: - """Whether the user simply did not opt in, or is on Python 3.9 where it is unavailable, Windows - must keep behaving exactly as it did before this feature. - """ + """When the user did not opt in, Windows must keep behaving exactly as it did before this feature.""" session = _windows_session(monkeypatch, installed=False) assert _mounts_legacy_adapter(session) is True From dad262ae3726dd93dc2c0f8841a0ec33d21c368c Mon Sep 17 00:00:00 2001 From: "omer.roth" Date: Wed, 7 Oct 2026 17:25:16 +0300 Subject: [PATCH 2/2] CM-74534 update CI cache key to rely on python version --- .github/workflows/docker-image.yml | 3 ++- .github/workflows/pre_release.yml | 3 ++- .github/workflows/release.yml | 3 ++- .github/workflows/ruff.yml | 3 ++- .github/workflows/tests.yml | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 919728b7..99939a42 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -31,6 +31,7 @@ jobs: git checkout ${{ steps.latest_tag.outputs.LATEST_TAG }} - name: Set up Python + id: setup-python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.10' @@ -40,7 +41,7 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.local - key: poetry-ubuntu-1 # increment to reset cache + key: poetry-ubuntu-${{ steps.setup-python.outputs.python-version }} - name: Setup Poetry if: steps.cached_poetry.outputs.cache-hit != 'true' diff --git a/.github/workflows/pre_release.yml b/.github/workflows/pre_release.yml index a84d01ed..e91a10b2 100644 --- a/.github/workflows/pre_release.yml +++ b/.github/workflows/pre_release.yml @@ -33,6 +33,7 @@ jobs: fetch-depth: 0 - name: Set up Python + id: setup-python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.10' @@ -42,7 +43,7 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.local - key: poetry-ubuntu-1 # increment to reset cache + key: poetry-ubuntu-${{ steps.setup-python.outputs.python-version }} - name: Setup Poetry if: steps.cached-poetry.outputs.cache-hit != 'true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0fa4682f..99790967 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,7 @@ jobs: fetch-depth: 0 - name: Set up Python + id: setup-python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.10' @@ -41,7 +42,7 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.local - key: poetry-ubuntu-1 # increment to reset cache + key: poetry-ubuntu-${{ steps.setup-python.outputs.python-version }} - name: Setup Poetry if: steps.cached-poetry.outputs.cache-hit != 'true' diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index a6a218f6..a72fbbe7 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -24,6 +24,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Python + id: setup-python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.10' @@ -33,7 +34,7 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.local - key: poetry-ubuntu-1 # increment to reset cache + key: poetry-ubuntu-${{ steps.setup-python.outputs.python-version }} - name: Setup Poetry if: steps.cached-poetry.outputs.cache-hit != 'true' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 72e38768..8f892fbf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,6 +26,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python + id: setup-python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.10' @@ -35,7 +36,7 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.local - key: poetry-ubuntu-1 # increment to reset cache + key: poetry-ubuntu-${{ steps.setup-python.outputs.python-version }} - name: Setup Poetry if: steps.cached-poetry.outputs.cache-hit != 'true'