diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 167a087..76bf21b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,7 +20,7 @@ on: dirctl_image_tag: required: false type: string - description: "Override the tag of the dir-ctl Docker image to use in the tests." + description: "Override the version (release tag) of dirctl used by the interoperability tests." permissions: contents: read diff --git a/.github/workflows/reusable-test-sdk.yaml b/.github/workflows/reusable-test-sdk.yaml index 64443d6..6634fc7 100644 --- a/.github/workflows/reusable-test-sdk.yaml +++ b/.github/workflows/reusable-test-sdk.yaml @@ -13,7 +13,7 @@ on: dirctl_image_tag: required: false type: string - description: "Override the tag of the dir-ctl Docker image to use in the tests." + description: "Override the version (release tag) of dirctl used by the interoperability tests." sdk_ref: required: false type: string @@ -70,7 +70,7 @@ jobs: - name: Test Python SDK env: DIR_CHART_VERSION: ${{ inputs.dir_chart_version }} - DIRCTL_IMAGE_TAG: ${{ inputs.dirctl_image_tag }} + DIRCTL_VERSION: ${{ inputs.dirctl_image_tag }} run: | task test diff --git a/CHANGELOG.md b/CHANGELOG.md index d4216e0..1134ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ [agntcy/dir]: https://github.com/agntcy/dir [agntcy/dir-sdk-python]: https://github.com/agntcy/dir-sdk-python +## Unreleased + +### Changed + +- **Breaking:** `client.sign` and `client.verify` now run natively in Python (cosign-compatible key signing, Sigstore keyless signing via the `sigstore` package, and local verification from the store referrers) instead of invoking `dirctl`. `Config.dirctl_path`, `Config.docker_config`, `DockerConfig` and the `DIRCTL_PATH`/`DIRCTL_IMAGE`/`DIRCTL_IMAGE_TAG` environment variables were removed. + ## 1.8.0 (2026-10-16) ### Changed diff --git a/README.md b/README.md index f64f722..5856a5d 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,8 @@ The Directory Python SDK provides comprehensive access to all Directory APIs wit - **Real-time Streaming**: Subscribe to Directory events with a streaming gRPC listener ### **Signing and Verification** -- **Local Signing**: Sign records locally using private keys or OIDC-based authentication. -Requires [dirctl](https://github.com/agntcy/dir/releases) binary to perform signing. -- **Remote Verification**: Verify record signatures using the Directory gRPC API +- **Native Signing**: Sign records in-process using cosign-compatible private keys or OIDC-based keyless signing (Sigstore). No external binary is required. +- **Local and Remote Verification**: Verify record signatures locally (key, OIDC identity, or any signer) or fetch the server's cached result with `from_server=True` ### **Developer Experience** - **Type Safety**: Full type hints for better IDE support and fewer runtime errors @@ -71,7 +70,6 @@ The SDK can be configured via environment variables or direct instantiation: ```python # Environment variables (insecure mode, default) export DIRECTORY_CLIENT_SERVER_ADDRESS="localhost:8888" -export DIRCTL_PATH="/path/to/dirctl" # Environment variables (X.509 authentication) export DIRECTORY_CLIENT_SERVER_ADDRESS="localhost:8888" @@ -89,15 +87,13 @@ from agntcy.dir_sdk.client import Config, Client # Insecure mode (default, for development only) config = Config( - server_address="localhost:8888", - dirctl_path="/usr/local/bin/dirctl" + server_address="localhost:8888" ) client = Client(config) # X.509 authentication with SPIRE x509_config = Config( server_address="localhost:8888", - dirctl_path="/usr/local/bin/dirctl", spiffe_socket_path="/tmp/agent.sock", auth_mode="x509" ) @@ -106,7 +102,6 @@ x509_client = Client(x509_config) # JWT authentication with SPIRE jwt_config = Config( server_address="localhost:8888", - dirctl_path="/usr/local/bin/dirctl", spiffe_socket_path="/tmp/agent.sock", auth_mode="jwt", jwt_audience="spiffe://example.org/dir-server" @@ -235,7 +230,6 @@ Common gRPC status codes: - Python 3.10 or higher - [uv](https://github.com/astral-sh/uv) - Package manager -- [dirctl](https://github.com/agntcy/dir/releases) - Directory CLI binary - Directory server instance (see setup below) ### 1. Server Setup diff --git a/RELEASE.md b/RELEASE.md index 1c6f765..4fc656b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -14,7 +14,7 @@ Create a branch for the new release: 2. Update the dependencies if necessary: - [agntcy-dir-grpc-python](https://buf.build/agntcy/dir/sdks/v1.8.0%3Agrpc/python) (Buf SDK) - [agntcy-dir-protocolbuffers-python](https://buf.build/agntcy/dir/sdks/v1.8.0%3Aprotocolbuffers/python) (Buf SDK) - - `.github/workflows/ci.yaml` (dir & dir-ctl version) + - `.github/workflows/ci.yaml` (dir version) 3. Add an entry to `CHANGELOG.md` ## 2. Create and push tags diff --git a/Taskfile.yml b/Taskfile.yml index 56a52d2..8b9bb5b 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -8,8 +8,6 @@ vars: DIR_CHART_VERSION: '{{ .DIR_CHART_VERSION | default "v1.8.0" }}' # renovate: datasource=github-releases depName=agntcy/dir versioning=semver DIR_IMAGE_TAG: '{{ .DIR_IMAGE_TAG | default "v1.8.0" }}' - # renovate: datasource=github-releases depName=agntcy/dir versioning=semver - DIRCTL_IMAGE_TAG: '{{ .DIRCTL_IMAGE_TAG | default "v1.8.0" }}' # renovate: datasource=github-releases depName=renovatebot/renovate versioning=semver RENOVATE_VERSION: "44.141.0" @@ -37,6 +35,10 @@ vars: COSIGN_VERSION: "3.1.3" COSIGN_BIN: "{{ .BIN_DIR }}/cosign-{{.COSIGN_VERSION}}" + # renovate: datasource=github-releases depName=agntcy/dir versioning=semver + DIRCTL_VERSION: '{{ .DIRCTL_VERSION | default "v1.8.0" }}' + DIRCTL_BIN: "{{ .BIN_DIR }}/dirctl-{{.DIRCTL_VERSION}}" + KIND_CLUSTER_NAME: "sdk-py-test" tasks: @@ -72,12 +74,12 @@ tasks: - defer: { task: test-env:delete } - task: test-env:create - | - export DIRCTL_IMAGE_TAG="{{ .DIRCTL_IMAGE_TAG }}" export COSIGN_PATH="$(printf "%s" "${COSIGN_PATH:-{{ .COSIGN_BIN }}}")" + export DIRCTL_PATH="$(printf "%s" "${DIRCTL_PATH:-{{ .DIRCTL_BIN }}}")" # Run SDK tests '{{ .UV_BIN }}' run pytest \ - --cov=agntcy.dir_sdk \ + --cov=dir-sdk-python/agntcy/dir_sdk \ --cov-report=xml:coverage.xml \ --cov-report=term-missing @@ -91,7 +93,7 @@ tasks: deps: - task: deps:uv - task: deps:cosign - - task: deps:dir-ctl + - task: deps:dirctl cmds: - "{{.UV_BIN}} sync --all-packages" @@ -393,22 +395,24 @@ tasks: deps:cosign: desc: Install sigstore cosign internal: true + deps: + - deps:bin-dir cmds: - curl -sfL https://github.com/sigstore/cosign/releases/download/v{{.COSIGN_VERSION}}/cosign-{{OS}}-{{ARCH}} -o {{.BIN_DIR}}/cosign-{{.COSIGN_VERSION}} - chmod +x {{.BIN_DIR}}/cosign-{{.COSIGN_VERSION}} status: - test -x {{.COSIGN_BIN}} - deps:dir-ctl: - desc: Install dir-ctl + deps:dirctl: + desc: Install dirctl (only used by the SDK interoperability tests) internal: true - run: once - vars: - DIRCTL_IMAGE_TAG: "{{ .DIRCTL_IMAGE_TAG }}" - preconditions: - - which docker + deps: + - deps:bin-dir cmds: - - docker image pull ghcr.io/agntcy/dir-ctl:{{ .DIRCTL_IMAGE_TAG }} + - curl -sfL https://github.com/agntcy/dir/releases/download/{{.DIRCTL_VERSION}}/dirctl-{{OS}}-{{ARCH}} -o {{.DIRCTL_BIN}} + - chmod +x {{.DIRCTL_BIN}} + status: + - test -x {{.DIRCTL_BIN}} deps:renovate: desc: Install Renovate diff --git a/dir-sdk-python/agntcy/dir_sdk/client/client.py b/dir-sdk-python/agntcy/dir_sdk/client/client.py index 2f642da..dec442e 100644 --- a/dir-sdk-python/agntcy/dir_sdk/client/client.py +++ b/dir-sdk-python/agntcy/dir_sdk/client/client.py @@ -82,7 +82,7 @@ def __init__(self, config: Config | None = None) -> None: self.routing_service = RoutingService(self.routing_client, logger) self.publication_service = PublicationService(self.publication_client, logger) self.search_service = SearchService(self.search_client, logger) - self.sign_service = SignService(self.config, self.sign_client, logger) + self.sign_service = SignService(self.sign_client, self.store_service, logger) self.sync_service = SyncService(self.sync_client, logger) self.event_service = EventService(self.event_client, logger) self.identity_service = IdentityService(self.identity_client, logger) diff --git a/dir-sdk-python/agntcy/dir_sdk/client/config.py b/dir-sdk-python/agntcy/dir_sdk/client/config.py index 8c68037..39c0f7b 100644 --- a/dir-sdk-python/agntcy/dir_sdk/client/config.py +++ b/dir-sdk-python/agntcy/dir_sdk/client/config.py @@ -31,69 +31,8 @@ def _parse_comma_scopes(value: str | list[str] | None, default: list[str]) -> li return [s.strip() for s in value.split(",") if s.strip()] -class DockerConfig: - """ - A class for using dirctl via the Docker image. - """ - - DEFAULT_DIRCTL_IMAGE = "ghcr.io/agntcy/dir-ctl" - DEFAULT_DIRCTL_IMAGE_TAG = "latest" - - def __init__( - self, - dirctl_image: str = DEFAULT_DIRCTL_IMAGE, - dirctl_image_tag: str = DEFAULT_DIRCTL_IMAGE_TAG, - envs: dict[str, str] | None = None, - mounts: list[str] | None = None, - user: str | None = None, - ) -> None: - if envs is None: - envs = {} - if mounts is None: - mounts = [] - - self.dirctl_image = dirctl_image - self.dirctl_image_tag = dirctl_image_tag - self.envs: dict[str, str] = envs - self.mounts: list[str] = mounts - self.user = user - - def get_commands(self) -> list[str]: - self.prune_mounts() - commands = [ - "docker", - "container", - "run", - "--name=dir-ctl", - "--rm", - "--network", - "host", - ] - if self.user: - commands.extend(["--user", self.user]) - for key, val in self.envs.items(): - commands.append("--env") - commands.append(f"{key}={val}") - for mount in self.mounts: - commands.append("--mount") - commands.append(mount) - commands.append(f"{self.dirctl_image}:{self.dirctl_image_tag}") - return commands - - def prune_mounts(self) -> None: - mounts = [] - for mount in self.mounts: - if mount.startswith("type=bind"): - _type, src, _dst = mount.split(",") - _, src = src.split("=") - if os.path.isfile(src): - mounts.append(mount) - self.mounts = mounts - - class Config: DEFAULT_SERVER_ADDRESS = "127.0.0.1:8888" - DEFAULT_DIRCTL_PATH = "dirctl" DEFAULT_SPIFFE_SOCKET_PATH = "" DEFAULT_AUTH_MODE = "" DEFAULT_AUTH_TOKEN: str = "" @@ -115,7 +54,6 @@ class Config: def __init__( self, server_address: str = DEFAULT_SERVER_ADDRESS, - dirctl_path: str | None = DEFAULT_DIRCTL_PATH, spiffe_socket_path: str = DEFAULT_SPIFFE_SOCKET_PATH, auth_mode: str = DEFAULT_AUTH_MODE, auth_token: str = DEFAULT_AUTH_TOKEN, @@ -133,10 +71,8 @@ def __init__( oidc_auth_timeout: float = DEFAULT_OIDC_AUTH_TIMEOUT, oidc_scopes: list[str] | None = None, oidc_access_token: str | None = None, - docker_config: DockerConfig | None = None, ) -> None: self.server_address = server_address - self.dirctl_path = dirctl_path self.spiffe_socket_path = spiffe_socket_path resolved_auth_token = auth_token or oidc_access_token or "" self.auth_token = resolved_auth_token @@ -160,33 +96,10 @@ def __init__( if oidc_scopes is not None else list(Config.DEFAULT_OIDC_SCOPES) ) - self.docker_config = docker_config - if dirctl_path and docker_config: - raise ValueError("You cannot specify both dirctl_path and docker_config.") - - def get_dirctl(self) -> list[str]: - if self.dirctl_path: - return [self.dirctl_path] - if self.docker_config is None: - msg = "Either dirctl_path or docker_config must be configured" - raise ValueError(msg) - return self.docker_config.get_commands() @staticmethod def load_from_env(env_prefix: str = "DIRECTORY_CLIENT_") -> "Config": """Load configuration from environment variables.""" - dirctl_path = os.environ.get("DIRCTL_PATH") - dirctl_image = os.environ.get("DIRCTL_IMAGE") - dirctl_image_tag = os.environ.get("DIRCTL_IMAGE_TAG") - - docker_config = None - if dirctl_image or dirctl_image_tag: - docker_config = DockerConfig( - dirctl_image or DockerConfig.DEFAULT_DIRCTL_IMAGE, - dirctl_image_tag or DockerConfig.DEFAULT_DIRCTL_IMAGE_TAG, - user="0:0", - ) - server_address = os.environ.get( f"{env_prefix}SERVER_ADDRESS", Config.DEFAULT_SERVER_ADDRESS, @@ -258,7 +171,6 @@ def load_from_env(env_prefix: str = "DIRECTORY_CLIENT_") -> "Config": return Config( server_address=server_address, - dirctl_path=dirctl_path, spiffe_socket_path=spiffe_socket_path, auth_mode=auth_mode, auth_token=auth_token, @@ -275,5 +187,4 @@ def load_from_env(env_prefix: str = "DIRECTORY_CLIENT_") -> "Config": oidc_callback_port=oidc_callback_port, oidc_auth_timeout=oidc_auth_timeout, oidc_scopes=oidc_scopes, - docker_config=docker_config, ) diff --git a/dir-sdk-python/agntcy/dir_sdk/client/dirctl/__init__.py b/dir-sdk-python/agntcy/dir_sdk/client/dirctl/__init__.py deleted file mode 100644 index 76d3a9b..0000000 --- a/dir-sdk-python/agntcy/dir_sdk/client/dirctl/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright AGNTCY Contributors (https://github.com/agntcy) -# SPDX-License-Identifier: Apache-2.0 - -"""dirctl command execution helpers.""" - -from agntcy.dir_sdk.client.dirctl.signing import sign_record -from agntcy.dir_sdk.client.dirctl.verification import verify_record - -__all__ = ["sign_record", "verify_record"] diff --git a/dir-sdk-python/agntcy/dir_sdk/client/dirctl/runner.py b/dir-sdk-python/agntcy/dir_sdk/client/dirctl/runner.py deleted file mode 100644 index a9ac2fc..0000000 --- a/dir-sdk-python/agntcy/dir_sdk/client/dirctl/runner.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright AGNTCY Contributors (https://github.com/agntcy) -# SPDX-License-Identifier: Apache-2.0 - -"""Low-level helpers for invoking the dirctl CLI safely.""" - -from __future__ import annotations - -import os -import subprocess -from collections.abc import Sequence - -from agntcy.dir_sdk.client.config import Config, DockerConfig - - -def _copy_docker_config(docker_config: DockerConfig) -> DockerConfig: - return DockerConfig( - dirctl_image=docker_config.dirctl_image, - dirctl_image_tag=docker_config.dirctl_image_tag, - envs=dict(docker_config.envs), - mounts=list(docker_config.mounts), - user=docker_config.user, - ) - - -def build_dirctl_base_command( - config: Config, - env: dict[str, str] | None = None, - extra_mounts: Sequence[str] | None = None, -) -> list[str]: - extra_mounts = list(extra_mounts or []) - if config.dirctl_path: - return [config.dirctl_path] - if config.docker_config is None: - msg = "Either dirctl_path or docker_config must be configured" - raise RuntimeError(msg) - - docker_config = _copy_docker_config(config.docker_config) - if env: - docker_config.envs.update(env) - docker_config.mounts.extend(extra_mounts) - return docker_config.get_commands() - - -def run_dirctl( - config: Config, - args: Sequence[str], - *, - env: dict[str, str] | None = None, - timeout: int = 60, - extra_mounts: Sequence[str] | None = None, -) -> None: - command = [ - *build_dirctl_base_command(config, env=env, extra_mounts=extra_mounts), - *args, - ] - shell_env = os.environ.copy() - if env: - shell_env.update(env) - - try: - subprocess.run( - command, - check=True, - capture_output=True, - env=shell_env, - timeout=timeout, - ) - except subprocess.CalledProcessError as e: - stderr = e.stderr.decode("utf-8", errors="ignore") - msg = f"dirctl command failed with return code {e.returncode}: {stderr}" - raise RuntimeError(msg) from e - except subprocess.TimeoutExpired as e: - msg = "dirctl command timed out" - raise RuntimeError(msg) from e diff --git a/dir-sdk-python/agntcy/dir_sdk/client/dirctl/signing.py b/dir-sdk-python/agntcy/dir_sdk/client/dirctl/signing.py deleted file mode 100644 index dabb6c8..0000000 --- a/dir-sdk-python/agntcy/dir_sdk/client/dirctl/signing.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright AGNTCY Contributors (https://github.com/agntcy) -# SPDX-License-Identifier: Apache-2.0 - -"""Signing helpers for dirctl-backed signature creation.""" - -from __future__ import annotations - -from agntcy.dir_sdk.client.config import Config -from agntcy.dir_sdk.client.dirctl.runner import run_dirctl -from agntcy.dir_sdk.models import core_v1, sign_v1 - - -def sign_record(config: Config, req: sign_v1.SignRequest) -> None: - if req.provider.HasField("key"): - _sign_with_key(config, req.record_ref, req.provider.key) - return - elif req.provider.HasField("oidc"): - _sign_with_oidc(config, req.record_ref, req.provider.oidc) - return - else: - msg = "Unsupported signing provider in request" - raise RuntimeError(msg) - - -def _sign_with_key( - config: Config, - record_ref: core_v1.RecordRef, - key_signer: sign_v1.SignWithKey, -) -> None: - password = "" - if key_signer.password: - password = key_signer.password.decode("utf-8") - run_dirctl( - config, - ["sign", record_ref.cid, "--key", key_signer.private_key], - env={ - "COSIGN_PASSWORD": password, - "DIRECTORY_CLIENT_SERVER_ADDRESS": config.server_address, - }, - ) - - -def _sign_with_oidc( - config: Config, - record_ref: core_v1.RecordRef, - oidc_signer: sign_v1.SignWithOIDC, -) -> None: - command = ["sign", record_ref.cid] - if oidc_signer.id_token: - command.extend(["--oidc-token", oidc_signer.id_token]) - if oidc_signer.options.oidc_provider_url: - command.extend(["--oidc-provider-url", oidc_signer.options.oidc_provider_url]) - if oidc_signer.options.oidc_client_id: - command.extend(["--oidc-client-id", oidc_signer.options.oidc_client_id]) - if oidc_signer.options.oidc_client_secret: - command.extend(["--oidc-client-secret", oidc_signer.options.oidc_client_secret]) - if oidc_signer.options.fulcio_url: - command.extend(["--fulcio-url", oidc_signer.options.fulcio_url]) - if oidc_signer.options.rekor_url: - command.extend(["--rekor-url", oidc_signer.options.rekor_url]) - if oidc_signer.options.timestamp_url: - command.extend(["--timestamp-url", oidc_signer.options.timestamp_url]) - if oidc_signer.options.skip_tlog: - command.append("--skip-tlog") - - run_dirctl( - config, command, env={"DIRECTORY_CLIENT_SERVER_ADDRESS": config.server_address} - ) diff --git a/dir-sdk-python/agntcy/dir_sdk/client/dirctl/verification.py b/dir-sdk-python/agntcy/dir_sdk/client/dirctl/verification.py deleted file mode 100644 index aaa07fb..0000000 --- a/dir-sdk-python/agntcy/dir_sdk/client/dirctl/verification.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright AGNTCY Contributors (https://github.com/agntcy) -# SPDX-License-Identifier: Apache-2.0 - -"""Verification helpers for dirctl-backed signature validation.""" - -from __future__ import annotations - -import contextlib -import json -import os -import tempfile - -from agntcy.dir_sdk.client.config import Config -from agntcy.dir_sdk.client.dirctl.runner import run_dirctl -from agntcy.dir_sdk.models import core_v1, sign_v1 -from google.protobuf import json_format - - -def verify_record(config: Config, req: sign_v1.VerifyRequest) -> sign_v1.VerifyResponse: - if req.record_ref is None or not req.record_ref.cid: - msg = "VerifyRequest.record_ref with cid is required" - raise RuntimeError(msg) - - fd, output_path = tempfile.mkstemp(suffix=".json", prefix="dirctl-verify-") - os.close(fd) - try: - _run_verify(config, req, output_path) - return parse_verify_response(output_path) - finally: - with contextlib.suppress(OSError): - os.unlink(output_path) - - -def _run_verify(config: Config, req: sign_v1.VerifyRequest, output_path: str) -> None: - extra_mounts: list[str] = [] - effective_output_path = output_path - if config.docker_config: - basename = os.path.basename(output_path) - extra_mounts.append(f"type=bind,src={output_path},dst=/{basename}") - effective_output_path = f"/{basename}" - - provider = req.provider - - if provider.HasField("key"): - _verify_with_key( - config, - req.record_ref, - provider.key, - effective_output_path, - extra_mounts=extra_mounts, - ) - elif provider.HasField("oidc"): - _verify_with_oidc( - config, - req.record_ref, - provider.oidc, - effective_output_path, - extra_mounts=extra_mounts, - ) - elif provider.HasField("any"): - _verify_with_any( - config, - req.record_ref, - provider.any, - effective_output_path, - extra_mounts=extra_mounts, - ) - else: - msg = "Unsupported verification provider in request" - raise RuntimeError(msg) - - -def _verify_with_key( - config: Config, - record_ref: core_v1.RecordRef, - key_verifier: sign_v1.VerifyWithKey, - output_path: str, - *, - extra_mounts: list[str], -) -> None: - run_dirctl( - config, - [ - "verify", - record_ref.cid, - "--key", - key_verifier.public_key, - "--output-file", - output_path, - ], - extra_mounts=extra_mounts, - env={"DIRECTORY_CLIENT_SERVER_ADDRESS": config.server_address}, - ) - - -def _verify_with_any( - config: Config, - record_ref: core_v1.RecordRef, - any_verifier: sign_v1.VerifyWithAny | None, - output_path: str, - *, - extra_mounts: list[str], -) -> None: - command = ["verify", record_ref.cid, "--output-file", output_path] - if any_verifier is not None and any_verifier.HasField("oidc_options"): - opts = any_verifier.oidc_options - if opts.tuf_mirror_url: - command.extend(["--tuf-mirror-url", opts.tuf_mirror_url]) - if opts.trusted_root_path: - command.extend(["--trusted-root-path", opts.trusted_root_path]) - if opts.ignore_tlog: - command.append("--ignore-tlog") - if opts.ignore_tsa: - command.append("--ignore-tsa") - if opts.ignore_sct: - command.append("--ignore-sct") - run_dirctl( - config, - command, - extra_mounts=extra_mounts, - env={"DIRECTORY_CLIENT_SERVER_ADDRESS": config.server_address}, - ) - - -def _verify_with_oidc( - config: Config, - record_ref: core_v1.RecordRef, - oidc_verifier: sign_v1.VerifyWithOIDC | None, - output_path: str, - *, - extra_mounts: list[str], -) -> None: - command = ["verify", record_ref.cid, "--output-file", output_path] - if oidc_verifier is not None: - if oidc_verifier.issuer: - command.extend(["--oidc-issuer", oidc_verifier.issuer]) - if oidc_verifier.subject: - command.extend(["--oidc-subject", oidc_verifier.subject]) - if oidc_verifier.HasField("options"): - opts = oidc_verifier.options - if opts.tuf_mirror_url: - command.extend(["--tuf-mirror-url", opts.tuf_mirror_url]) - if opts.trusted_root_path: - command.extend(["--trusted-root-path", opts.trusted_root_path]) - if opts.ignore_tlog: - command.append("--ignore-tlog") - if opts.ignore_tsa: - command.append("--ignore-tsa") - if opts.ignore_sct: - command.append("--ignore-sct") - run_dirctl( - config, - command, - extra_mounts=extra_mounts, - env={"DIRECTORY_CLIENT_SERVER_ADDRESS": config.server_address}, - ) - - -def parse_verify_response(output_path: str) -> sign_v1.VerifyResponse: - try: - with open(output_path, "rb") as f: - output = f.read().decode("utf-8") - - json_data = json.loads(output) - response = sign_v1.VerifyResponse() - json_format.ParseDict(json_data, response) - - return response - except (json.JSONDecodeError, UnicodeDecodeError) as e: - msg = f"Failed to parse verification response: {e}" - raise RuntimeError(msg) from e diff --git a/dir-sdk-python/agntcy/dir_sdk/client/services/signing.py b/dir-sdk-python/agntcy/dir_sdk/client/services/signing.py index a8e063e..4fba727 100644 --- a/dir-sdk-python/agntcy/dir_sdk/client/services/signing.py +++ b/dir-sdk-python/agntcy/dir_sdk/client/services/signing.py @@ -9,22 +9,21 @@ from collections.abc import Sequence import grpc -from agntcy.dir_sdk.client.config import Config -from agntcy.dir_sdk.client.dirctl.signing import sign_record -from agntcy.dir_sdk.client.dirctl.verification import verify_record from agntcy.dir_sdk.client.services.base import RpcServiceBase +from agntcy.dir_sdk.client.services.store import StoreService +from agntcy.dir_sdk.client.signing import sign_record, verify_record from agntcy.dir_sdk.models import sign_v1 class SignService(RpcServiceBase): def __init__( self, - config: Config, sign_client: sign_v1.SignServiceStub, + store_service: StoreService, logger: logging.Logger, ) -> None: super().__init__(logger) - self._config = config + self._store_service = store_service self._sign_client = sign_client def verify( @@ -45,14 +44,14 @@ def verify( self._logger.exception("Verification failed") raise RuntimeError(f"Verify failed: {e}") from e try: - return verify_record(self._config, req) + return verify_record(self._store_service, req, metadata=metadata) except Exception as e: self._logger.exception("Verification operation failed") raise RuntimeError(f"Failed to verify the object: {e}") from e def sign(self, req: sign_v1.SignRequest) -> None: try: - sign_record(self._config, req) + sign_record(self._store_service, req) except RuntimeError as e: raise RuntimeError(f"Failed to sign the object: {e}") from e except Exception as e: diff --git a/dir-sdk-python/agntcy/dir_sdk/client/signing/__init__.py b/dir-sdk-python/agntcy/dir_sdk/client/signing/__init__.py new file mode 100644 index 0000000..24b5b2e --- /dev/null +++ b/dir-sdk-python/agntcy/dir_sdk/client/signing/__init__.py @@ -0,0 +1,9 @@ +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +"""Native (dirctl-free) record signing and verification.""" + +from agntcy.dir_sdk.client.signing.sign import sign_record +from agntcy.dir_sdk.client.signing.verify import verify_record + +__all__ = ["sign_record", "verify_record"] diff --git a/dir-sdk-python/agntcy/dir_sdk/client/signing/ed25519ph.py b/dir-sdk-python/agntcy/dir_sdk/client/signing/ed25519ph.py new file mode 100644 index 0000000..0fd1ba4 --- /dev/null +++ b/dir-sdk-python/agntcy/dir_sdk/client/signing/ed25519ph.py @@ -0,0 +1,51 @@ +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +"""Ed25519ph (RFC 8032, section 5.1) sign and verify backed by libsodium. + +Cosign loads Ed25519 private keys as Ed25519ph signers (the message is +pre-hashed with SHA-512 and the dom2 prefix is applied). The `cryptography` +package only exposes pure Ed25519, so the pre-hashed variant is taken from +PyNaCl's libsodium bindings. +""" + +from __future__ import annotations + +import nacl.bindings as sodium +from nacl.exceptions import BadSignatureError + +_SEED_BYTES = sodium.crypto_sign_SEEDBYTES +_PUBLIC_KEY_BYTES = sodium.crypto_sign_PUBLICKEYBYTES +_SIGNATURE_BYTES = sodium.crypto_sign_BYTES + + +def public_key(seed: bytes) -> bytes: + """Derive the raw 32-byte public key from a 32-byte private seed.""" + return _keypair(seed)[0] + + +def _keypair(seed: bytes) -> tuple[bytes, bytes]: + if len(seed) != _SEED_BYTES: + msg = "Ed25519 seed must be 32 bytes" + raise ValueError(msg) + return sodium.crypto_sign_seed_keypair(seed) + + +def sign(seed: bytes, message: bytes) -> bytes: + """Create an Ed25519ph signature over message.""" + _, secret_key = _keypair(seed) + state = sodium.crypto_sign_ed25519ph_state() + sodium.crypto_sign_ed25519ph_update(state, message) + return sodium.crypto_sign_ed25519ph_final_create(state, secret_key) + + +def verify(pub: bytes, message: bytes, signature: bytes) -> bool: + """Verify an Ed25519ph signature over message.""" + if len(pub) != _PUBLIC_KEY_BYTES or len(signature) != _SIGNATURE_BYTES: + return False + state = sodium.crypto_sign_ed25519ph_state() + sodium.crypto_sign_ed25519ph_update(state, message) + try: + return bool(sodium.crypto_sign_ed25519ph_final_verify(state, signature, pub)) + except BadSignatureError: + return False diff --git a/dir-sdk-python/agntcy/dir_sdk/client/signing/keys.py b/dir-sdk-python/agntcy/dir_sdk/client/signing/keys.py new file mode 100644 index 0000000..6d62ee5 --- /dev/null +++ b/dir-sdk-python/agntcy/dir_sdk/client/signing/keys.py @@ -0,0 +1,238 @@ +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +"""Key-based signing and verification compatible with cosign key material. + +Mirrors the Go client behavior (``client/utils/cosign``): + +* private keys are loaded as raw PEM first (cosign encrypted keys or plain + PKCS#8/PKCS#1/SEC1 PEM) and then as a key reference (file path); +* ECDSA and RSA keys sign/verify with SHA-256 (ASN.1 DER ECDSA, PKCS#1 v1.5 RSA); +* Ed25519 keys sign as Ed25519ph (cosign's default load options) and verify as + pure Ed25519 first, falling back to Ed25519ph. +""" + +from __future__ import annotations + +import base64 +import binascii +import hashlib +import json +import os + +from agntcy.dir_sdk.client.signing import ed25519ph +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa +from nacl.exceptions import CryptoError +from nacl.secret import SecretBox + +PrivateKey = ec.EllipticCurvePrivateKey | rsa.RSAPrivateKey | ed25519.Ed25519PrivateKey +PublicKey = ec.EllipticCurvePublicKey | rsa.RSAPublicKey | ed25519.Ed25519PublicKey + +# PEM block types used by cosign for password-protected private keys. +_ENCRYPTED_PEM_TYPES = ( + "ENCRYPTED SIGSTORE PRIVATE KEY", + "ENCRYPTED COSIGN PRIVATE KEY", +) +_PEM_PREFIX = "-----BEGIN " +_SCRYPT_MAX_MEM = 512 * 1024 * 1024 +_CURVE_NAMES = { + "secp256r1": "P-256", + "secp384r1": "P-384", + "secp521r1": "P-521", +} + + +class KeyLoadError(Exception): + """Raised when key material cannot be loaded or used.""" + + +def _decrypt_cosign_key(der: bytes, password: bytes) -> bytes: + try: + envelope = json.loads(der) + kdf = envelope["kdf"] + cipher = envelope["cipher"] + if kdf["name"] != "scrypt": + msg = f"unsupported KDF {kdf['name']!r}" + raise KeyLoadError(msg) + if cipher["name"] != "nacl/secretbox": + msg = f"unsupported cipher {cipher['name']!r}" + raise KeyLoadError(msg) + params = kdf["params"] + salt = base64.b64decode(kdf["salt"]) + nonce = base64.b64decode(cipher["nonce"]) + ciphertext = base64.b64decode(envelope["ciphertext"]) + key = hashlib.scrypt( + password, + salt=salt, + n=int(params["N"]), + r=int(params["r"]), + p=int(params["p"]), + maxmem=_SCRYPT_MAX_MEM, + dklen=32, + ) + except (KeyError, TypeError, ValueError, binascii.Error) as e: + msg = f"invalid encrypted key format: {e}" + raise KeyLoadError(msg) from e + + try: + return SecretBox(key).decrypt(ciphertext, nonce) + except (CryptoError, ValueError) as e: + msg = "decrypt: wrong password or corrupted key" + raise KeyLoadError(msg) from e + + +def _load_private_key_pem(data: bytes, password: bytes) -> PrivateKey: + text = data.decode("utf-8", errors="ignore") + for pem_type in _ENCRYPTED_PEM_TYPES: + if f"{_PEM_PREFIX}{pem_type}-----" in text: + body = "".join( + line + for line in text.splitlines() + if line and not line.startswith("-----") + ) + try: + der = base64.b64decode(body) + except binascii.Error as e: + msg = f"invalid PEM body: {e}" + raise KeyLoadError(msg) from e + pkcs8 = _decrypt_cosign_key(der, password) + key = serialization.load_der_private_key(pkcs8, None) + return _ensure_supported_private(key) + + try: + key = serialization.load_pem_private_key(data, password or None) + except (ValueError, TypeError) as e: + msg = f"unsupported private key: {e}" + raise KeyLoadError(msg) from e + return _ensure_supported_private(key) + + +def _ensure_supported_private(key: object) -> PrivateKey: + if isinstance( + key, ec.EllipticCurvePrivateKey | rsa.RSAPrivateKey | ed25519.Ed25519PrivateKey + ): + return key + msg = f"unsupported private key type: {type(key).__name__}" + raise KeyLoadError(msg) + + +def _ensure_supported_public(key: object) -> PublicKey: + if isinstance( + key, ec.EllipticCurvePublicKey | rsa.RSAPublicKey | ed25519.Ed25519PublicKey + ): + return key + msg = f"unsupported public key type: {type(key).__name__}" + raise KeyLoadError(msg) + + +def _read_key_ref(ref: str) -> bytes: + if ref.startswith(_PEM_PREFIX): + msg = "invalid PEM content" + raise KeyLoadError(msg) + path = os.path.expanduser(ref) + try: + with open(path, "rb") as f: + return f.read() + except OSError as e: + msg = f"unsupported key reference {ref!r}: {e}" + raise KeyLoadError(msg) from e + + +def load_private_key(private_key: str, password: bytes = b"") -> PrivateKey: + """Load a private key from raw PEM content or a key reference (file path).""" + try: + return _load_private_key_pem(private_key.encode("utf-8"), password) + except KeyLoadError as raw_err: + if private_key.lstrip().startswith(_PEM_PREFIX): + raise + try: + data = _read_key_ref(private_key) + except KeyLoadError: + raise raw_err from None + return _load_private_key_pem(data, password) + + +def load_public_key(public_key: str) -> PublicKey: + """Load a public key from raw PEM content or a key reference (file path).""" + try: + return _ensure_supported_public( + serialization.load_pem_public_key(public_key.encode("utf-8")) + ) + except (ValueError, TypeError, KeyLoadError) as raw_err: + if public_key.lstrip().startswith(_PEM_PREFIX): + msg = f"failed to load public key: {raw_err}" + raise KeyLoadError(msg) from raw_err + data = _read_key_ref(public_key) + try: + return _ensure_supported_public(serialization.load_pem_public_key(data)) + except (ValueError, TypeError) as e: + msg = f"failed to load public key: {e}" + raise KeyLoadError(msg) from e + + +def public_key_pem(key: PrivateKey | PublicKey) -> str: + """Return the PKIX PEM encoding of the (public part of the) key.""" + pub: PublicKey = ( + key + if isinstance( + key, + ec.EllipticCurvePublicKey | rsa.RSAPublicKey | ed25519.Ed25519PublicKey, + ) + else key.public_key() + ) + return pub.public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ).decode("ascii") + + +def sign_message(key: PrivateKey, payload: bytes) -> bytes: + """Sign payload the same way cosign's default signer/verifier would.""" + if isinstance(key, ec.EllipticCurvePrivateKey): + return key.sign(payload, ec.ECDSA(hashes.SHA256())) + if isinstance(key, rsa.RSAPrivateKey): + return key.sign(payload, padding.PKCS1v15(), hashes.SHA256()) + seed = key.private_bytes( + serialization.Encoding.Raw, + serialization.PrivateFormat.Raw, + serialization.NoEncryption(), + ) + return ed25519ph.sign(seed, payload) + + +def verify_message(key: PublicKey, signature: bytes, payload: bytes) -> bool: + """Verify signature over payload; Ed25519 falls back to Ed25519ph.""" + try: + if isinstance(key, ec.EllipticCurvePublicKey): + key.verify(signature, payload, ec.ECDSA(hashes.SHA256())) + elif isinstance(key, rsa.RSAPublicKey): + key.verify(signature, payload, padding.PKCS1v15(), hashes.SHA256()) + else: + try: + key.verify(signature, payload) + except InvalidSignature: + raw = key.public_bytes( + serialization.Encoding.Raw, serialization.PublicFormat.Raw + ) + return ed25519ph.verify(raw, payload, signature) + except InvalidSignature: + return False + return True + + +def detect_key_algorithm(public_key_pem_str: str) -> str: + """Return e.g. ECDSA-P-256, Ed25519 or RSA-2048 for a PEM public key.""" + try: + key = serialization.load_pem_public_key(public_key_pem_str.encode("utf-8")) + except (ValueError, TypeError): + return "unknown" + if isinstance(key, ec.EllipticCurvePublicKey): + name = _CURVE_NAMES.get(key.curve.name) + return f"ECDSA-{name}" if name else "ECDSA" + if isinstance(key, ed25519.Ed25519PublicKey): + return "Ed25519" + if isinstance(key, rsa.RSAPublicKey): + return f"RSA-{key.key_size}" + return "unknown" diff --git a/dir-sdk-python/agntcy/dir_sdk/client/signing/oidc.py b/dir-sdk-python/agntcy/dir_sdk/client/signing/oidc.py new file mode 100644 index 0000000..1a3aa3e --- /dev/null +++ b/dir-sdk-python/agntcy/dir_sdk/client/signing/oidc.py @@ -0,0 +1,495 @@ +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +"""Keyless (OIDC / Sigstore bundle) signing and verification. + +Mirrors the Go client (``client/utils/cosign``) on top of the ``sigstore`` +Python package: signing produces a Sigstore bundle with an ephemeral key, a +Fulcio certificate, an optional Rekor entry and RFC 3161 timestamps; verification +follows the sigstore-go verifier configuration used by the Go client. +""" + +from __future__ import annotations + +import base64 +import hashlib +import logging +import re +from collections.abc import Sequence +from dataclasses import dataclass +from datetime import datetime, timezone + +from agntcy.dir_sdk.models import sign_v1 +from cryptography import x509 +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa +from cryptography.hazmat.primitives.asymmetric.utils import Prehashed +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat +from OpenSSL.crypto import ( + X509, + X509Store, + X509StoreContext, + X509StoreContextError, + X509StoreFlags, +) +from rfc3161_client import TimeStampResponse, VerifierBuilder +from rfc3161_client import VerificationError as Rfc3161VerificationError +from sigstore._internal.fulcio.client import FulcioClient +from sigstore._internal.rekor.client import RekorClient +from sigstore._internal.sct import verify_sct +from sigstore._internal.timestamp import TimestampAuthorityClient +from sigstore._internal.trust import KeyringPurpose +from sigstore._internal.tuf import TrustUpdater +from sigstore._utils import sha256_digest +from sigstore.errors import Error as SigstoreError +from sigstore.models import Bundle, TimestampVerificationData, TrustedRoot +from sigstore.oidc import IdentityToken, Issuer +from sigstore.verify.verifier import ( + _validate_hashedrekord_v001_entry_body, + _validate_hashedrekord_v002_entry_body, +) +from sigstore_models.bundle import v1 as bundle_v1 +from sigstore_models.common import v1 as common_v1 +from sigstore_models.common.v1 import HashOutput, MessageSignature + +logger = logging.getLogger("client") + +DEFAULT_FULCIO_URL = "https://fulcio.sigstore.dev" +DEFAULT_REKOR_URL = "https://rekor.sigstore.dev" +DEFAULT_TIMESTAMP_URL = "https://timestamp.sigstore.dev/api/v1/timestamp" +DEFAULT_TUF_MIRROR_URL = "https://tuf-repo-cdn.sigstore.dev" +DEFAULT_OIDC_PROVIDER_URL = "https://oauth2.sigstore.dev/auth" +DEFAULT_OIDC_CLIENT_ID = "sigstore" + +_OID_ISSUER_V1 = x509.ObjectIdentifier("1.3.6.1.4.1.57264.1.1") +_OID_ISSUER_V2 = x509.ObjectIdentifier("1.3.6.1.4.1.57264.1.8") +_OID_OTHER_NAME_SAN = x509.ObjectIdentifier("1.3.6.1.4.1.57264.1.7") + +_UTF8_STRING_TAG = 0x0C +_DER_LONG_FORM = 0x80 + + +class OIDCError(Exception): + """Raised for keyless signing and verification failures.""" + + +# --------------------------------------------------------------------------- +# Signing +# --------------------------------------------------------------------------- + + +def _identity_token(id_token: str, opts: sign_v1.SignOptionsOIDC) -> IdentityToken: + client_id = opts.oidc_client_id or DEFAULT_OIDC_CLIENT_ID + if id_token: + return IdentityToken(id_token, client_id=client_id) + # No token supplied: run the interactive OIDC flow against the provider. + issuer = Issuer(opts.oidc_provider_url or DEFAULT_OIDC_PROVIDER_URL) + return issuer.identity_token( + client_id=client_id, client_secret=opts.oidc_client_secret + ) + + +def sign_blob_with_oidc( + payload: bytes, + req: sign_v1.SignWithOIDC, +) -> tuple[sign_v1.Signature, sign_v1.PublicKey]: + """Sign payload with an ephemeral key and a Fulcio certificate.""" + signing_time = datetime.now(timezone.utc) + opts = req.options + fulcio_url = opts.fulcio_url or DEFAULT_FULCIO_URL + rekor_url = opts.rekor_url or DEFAULT_REKOR_URL + timestamp_url = opts.timestamp_url or DEFAULT_TIMESTAMP_URL + + try: + identity = _identity_token(req.id_token, opts) + + key = ec.generate_private_key(ec.SECP256R1()) + public_key_pem = key.public_key().public_bytes( + Encoding.PEM, PublicFormat.SubjectPublicKeyInfo + ) + + # Fulcio takes the identity from the OIDC token, the CSR subject is unused. + csr = ( + x509.CertificateSigningRequestBuilder() + .subject_name(x509.Name([])) + .add_extension(x509.BasicConstraints(ca=False, path_length=None), True) + .sign(key, hashes.SHA256()) + ) + certificate = FulcioClient(fulcio_url).signing_cert.post(csr, identity).cert + + hashed = sha256_digest(payload) + raw_signature = key.sign(hashed.digest, ec.ECDSA(hashed._as_prehashed())) + content = MessageSignature( + message_digest=HashOutput( + algorithm=hashed.algorithm, + digest=base64.b64encode(hashed.digest), + ), + signature=base64.b64encode(raw_signature), + ) + + timestamps = [ + TimestampAuthorityClient(timestamp_url).request_timestamp(raw_signature) + ] + + if opts.skip_tlog: + bundle_json = _bundle_without_tlog(certificate, content, timestamps) + media_type = Bundle.BundleType.BUNDLE_0_3.value + else: + rekor = RekorClient(rekor_url) + entry = rekor.create_entry( + rekor._build_hashed_rekord_request( + hashed_input=hashed, + signature=raw_signature, + certificate=certificate, + ) + ) + bundle = Bundle._from_parts(certificate, content, entry, timestamps) + bundle_json = bundle.to_json() + media_type = bundle._inner.media_type + except Exception as e: + msg = f"failed to sign record: {e}" + raise OIDCError(msg) from e + + signature = sign_v1.Signature( + signature=base64.b64encode(raw_signature).decode("ascii"), + certificate=base64.b64encode(certificate.public_bytes(Encoding.DER)).decode( + "ascii" + ), + content_type=str(media_type), + content_bundle=bundle_json, + signed_at=signing_time.strftime("%Y-%m-%dT%H:%M:%SZ"), + ) + return signature, sign_v1.PublicKey(key=public_key_pem.decode("ascii")) + + +def _bundle_without_tlog( + certificate: x509.Certificate, + content: MessageSignature, + timestamps: Sequence[TimeStampResponse], +) -> str: + """Build a v0.3 bundle without a transparency log entry (skip_tlog).""" + inner = bundle_v1.Bundle( + media_type=Bundle.BundleType.BUNDLE_0_3.value, + verification_material=bundle_v1.VerificationMaterial( + certificate=common_v1.X509Certificate( + raw_bytes=base64.b64encode(certificate.public_bytes(Encoding.DER)) + ), + tlog_entries=[], + timestamp_verification_data=bundle_v1.TimestampVerificationData( + rfc3161_timestamps=[ + common_v1.RFC3161SignedTimestamp( + signed_timestamp=base64.b64encode(ts.as_bytes()) + ) + for ts in timestamps + ] + ), + ), + message_signature=content, + ) + return inner.to_json() + + +# --------------------------------------------------------------------------- +# Verification +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class _VerifiedTime: + source: str + time: datetime + + +def _load_trusted_root(opts: sign_v1.VerifyOptionsOIDC) -> TrustedRoot: + if opts.trusted_root_path: + try: + return TrustedRoot.from_file(opts.trusted_root_path) + except Exception as e: + msg = f"failed to load trusted root from path {opts.trusted_root_path}: {e}" + raise OIDCError(msg) from e + + tuf_url = opts.tuf_mirror_url or DEFAULT_TUF_MIRROR_URL + try: + updater = TrustUpdater(tuf_url) + return TrustedRoot.from_file(updater.get_trusted_root_path()) + except Exception as e: + msg = f"failed to fetch trusted root from TUF: {e}" + raise OIDCError(msg) from e + + +def _verify_signed_timestamp( + trusted_root: TrustedRoot, + response: TimeStampResponse, + message: bytes, +) -> _VerifiedTime | None: + for authority in trusted_root.get_timestamp_authorities(): + certificates = authority.certificates(allow_expired=True) + # A signing certificate and a root are required, intermediates optional. + if len(certificates) < 2: + continue + + builder = ( + VerifierBuilder() + .tsa_certificate(certificates[0]) + .add_root_certificate(certificates[-1]) + ) + for intermediate in certificates[1:-1]: + builder = builder.add_intermediate_certificate(intermediate) + try: + builder.build().verify_message(response, message) + except Rfc3161VerificationError: + continue + + gen_time = response.tst_info.gen_time + end = authority.validity_period_end + if authority.validity_period_start <= gen_time and ( + end is None or gen_time < end + ): + return _VerifiedTime("TimestampAuthority", gen_time) + return None + + +def _verify_chain_at_time( + trusted_root: TrustedRoot, + leaf: x509.Certificate, + intermediates: Sequence[x509.Certificate], + at: datetime, +) -> list[x509.Certificate]: + store = X509Store() + store.set_flags(X509StoreFlags.X509_STRICT) + for ca in trusted_root.get_fulcio_certs(): + store.add_cert(X509.from_cryptography(ca)) + store.set_time(at) + context = X509StoreContext( + store, + X509.from_cryptography(leaf), + chain=[X509.from_cryptography(c) for c in intermediates], + ) + try: + verified = context.get_verified_chain() + except X509StoreContextError as e: + msg = f"failed to verify leaf certificate: {e}" + raise OIDCError(msg) from e + return [c.to_cryptography() for c in verified[1:]] + + +def _der_utf8_string(data: bytes) -> str: + if len(data) < 2 or data[0] != _UTF8_STRING_TAG: + msg = "unexpected OtherName encoding" + raise OIDCError(msg) + length = data[1] + offset = 2 + if length & _DER_LONG_FORM: + count = length & ~_DER_LONG_FORM + length = int.from_bytes(data[offset : offset + count], "big") + offset += count + return data[offset : offset + length].decode("utf-8") + + +@dataclass(frozen=True) +class _CertSummary: + issuer: str + subject: str + certificate_issuer: str + + +def _summarize_certificate(cert: x509.Certificate) -> _CertSummary: + issuer = "" + for ext in cert.extensions: + raw = ( + ext.value.value + if isinstance(ext.value, x509.UnrecognizedExtension) + else b"" + ) + if ext.oid == _OID_ISSUER_V1 and not issuer: + issuer = raw.decode("utf-8", errors="replace") + elif ext.oid == _OID_ISSUER_V2: + issuer = _der_utf8_string(raw) + + subject = "" + try: + san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value + except x509.ExtensionNotFound: + san = None + if san is not None: + uris = san.get_values_for_type(x509.UniformResourceIdentifier) + emails = san.get_values_for_type(x509.RFC822Name) + if uris: + subject = uris[0] + elif emails: + subject = emails[0] + else: + for other in san.get_values_for_type(x509.OtherName): + if other.type_id == _OID_OTHER_NAME_SAN: + subject = _der_utf8_string(other.value) + break + if not subject: + msg = "no Subject Alternative Name found" + raise OIDCError(msg) + + return _CertSummary( + issuer=issuer, + subject=subject, + certificate_issuer=cert.issuer.rfc4514_string(), + ) + + +def get_value_matcher(value: str) -> tuple[str, str]: + """Return (exact, regexp) matchers for value, as the Go client does.""" + if value == "": + return "", ".*" + try: + re.compile(value) + except re.error: + return value, "" + return "", value + + +def _matches(exact: str, pattern: str, actual: str) -> bool: + if exact and actual != exact: + return False + return not (pattern and re.search(pattern, actual) is None) + + +def _verify_signature(cert: x509.Certificate, signature: bytes, digest: bytes) -> None: + key = cert.public_key() + try: + if isinstance(key, ec.EllipticCurvePublicKey): + key.verify(signature, digest, ec.ECDSA(Prehashed(hashes.SHA256()))) + elif isinstance(key, rsa.RSAPublicKey): + key.verify( + signature, digest, padding.PKCS1v15(), Prehashed(hashes.SHA256()) + ) + else: + msg = f"unsupported certificate public key type: {type(key).__name__}" + raise OIDCError(msg) + except InvalidSignature as e: + msg = "failed to verify signature: signature is invalid for input" + raise OIDCError(msg) from e + + +def verify_with_oidc( + payload: bytes, + req: sign_v1.VerifyWithOIDC, + signature: sign_v1.Signature, +) -> sign_v1.SignerInfo: + """Verify a Sigstore bundle against the OIDC identity in req.""" + opts = req.options if req.HasField("options") else sign_v1.VerifyOptionsOIDC() + + try: + bundle_inner = bundle_v1.Bundle.from_json(signature.content_bundle) + except Exception as e: + msg = f"failed to parse bundle: {e}" + raise OIDCError(msg) from e + + trusted_root = _load_trusted_root(opts) + + material = bundle_inner.verification_material + if bundle_inner.message_signature is None: + msg = "failed to verify signature: bundle has no message signature" + raise OIDCError(msg) + raw_signature = bundle_inner.message_signature.signature + + # Leaf certificate (and intermediates for pre-v0.3 bundles). + intermediates: list[x509.Certificate] = [] + if material.certificate is not None: + leaf = x509.load_der_x509_certificate(material.certificate.raw_bytes) + elif ( + material.x509_certificate_chain and material.x509_certificate_chain.certificates + ): + certs = [ + x509.load_der_x509_certificate(c.raw_bytes) + for c in material.x509_certificate_chain.certificates + ] + leaf, intermediates = certs[0], certs[1:] + else: + msg = "bundle does not contain a signing certificate" + raise OIDCError(msg) + + digest = hashlib.sha256(payload).digest() + if ( + bundle_inner.message_signature.message_digest is not None + and bundle_inner.message_signature.message_digest.digest != digest + ): + msg = "failed to verify signature: message digest mismatch" + raise OIDCError(msg) + + # Transparency log (required unless ignore_tlog). + if not opts.ignore_tlog: + try: + bundle = Bundle(bundle_inner) + entry = bundle.log_entry + entry._verify(trusted_root.rekor_keyring(KeyringPurpose.VERIFY)) + hashed = sha256_digest(payload) + version = entry._inner.kind_version.version + if entry._inner.kind_version.kind != "hashedrekord": + msg = f"expected entry type hashedrekord, got {entry._inner.kind_version.kind}" + raise OIDCError(msg) + if version == "0.0.2": + _validate_hashedrekord_v002_entry_body(bundle, hashed) + else: + _validate_hashedrekord_v001_entry_body(bundle, hashed) + except (SigstoreError, ValueError, KeyError) as e: + msg = f"failed to verify log inclusion: {e}" + raise OIDCError(msg) from e + except OIDCError: + raise + + # Observer timestamps. + verified_times: list[_VerifiedTime] = [] + if not opts.ignore_tsa: + responses = ( + TimestampVerificationData( + material.timestamp_verification_data + ).rfc3161_timestamps + if material.timestamp_verification_data is not None + else [] + ) + for response in responses: + verified = _verify_signed_timestamp(trusted_root, response, raw_signature) + if verified is not None: + verified_times.append(verified) + if not verified_times: + msg = "failed to verify timestamps: threshold not met for verified signed timestamps: 0 < 1" + raise OIDCError(msg) + if opts.ignore_tlog: + verified_times.append(_VerifiedTime("CurrentTime", datetime.now(timezone.utc))) + if not verified_times: + msg = "failed to verify timestamps: no valid observer timestamps found" + raise OIDCError(msg) + + # Certificate chain must validate at every verified time. + chain: list[x509.Certificate] = [] + summary = _summarize_certificate(leaf) + for verified_time in verified_times: + chain = _verify_chain_at_time( + trusted_root, leaf, intermediates, verified_time.time + ) + + if not opts.ignore_sct: + try: + verify_sct(leaf, chain, trusted_root.ct_keyring(KeyringPurpose.VERIFY)) + except (SigstoreError, ValueError) as e: + msg = f"failed to verify signed certificate timestamp: {e}" + raise OIDCError(msg) from e + + _verify_signature(leaf, raw_signature, digest) + + # Identity policy. + issuer, issuer_regexp = get_value_matcher(req.issuer) + subject, subject_regexp = get_value_matcher(req.subject) + if not _matches(issuer, issuer_regexp, summary.issuer): + msg = f"failed to verify certificate identity: no matching issuer, got {summary.issuer!r}" + raise OIDCError(msg) + if not _matches(subject, subject_regexp, summary.subject): + msg = f"failed to verify certificate identity: no matching subject alternative name, got {summary.subject!r}" + raise OIDCError(msg) + + return sign_v1.SignerInfo( + oidc=sign_v1.SignerInfoOIDC( + issuer=summary.issuer, + subject=summary.subject, + certificate_issuer=summary.certificate_issuer, + ) + ) diff --git a/dir-sdk-python/agntcy/dir_sdk/client/signing/referrer.py b/dir-sdk-python/agntcy/dir_sdk/client/signing/referrer.py new file mode 100644 index 0000000..c1805c8 --- /dev/null +++ b/dir-sdk-python/agntcy/dir_sdk/client/signing/referrer.py @@ -0,0 +1,64 @@ +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +"""Referrer encoding for signatures and public keys. + +Mirrors ``MarshalReferrer``/``UnmarshalReferrer`` from the Go +``api/sign/v1`` package: the message is encoded with protojson (camelCase +field names, default values omitted) into a ``google.protobuf.Struct`` and the +referrer ``type`` is the protobuf full name of the message. +""" + +from __future__ import annotations + +from agntcy.dir_sdk.models import core_v1, sign_v1 +from google.protobuf import json_format, struct_pb2 + +SIGNATURE_REFERRER_TYPE = sign_v1.Signature.DESCRIPTOR.full_name +PUBLIC_KEY_REFERRER_TYPE = sign_v1.PublicKey.DESCRIPTOR.full_name + + +def _marshal(message: sign_v1.Signature | sign_v1.PublicKey) -> struct_pb2.Struct: + data = struct_pb2.Struct() + json_format.ParseDict(json_format.MessageToDict(message), data) + return data + + +def _unmarshal( + referrer: core_v1.RecordReferrer, + message: sign_v1.Signature | sign_v1.PublicKey, +) -> None: + if referrer is None or not referrer.HasField("data"): + msg = "referrer or data is nil" + raise ValueError(msg) + json_format.ParseDict(json_format.MessageToDict(referrer.data), message) + + +def marshal_signature(signature: sign_v1.Signature) -> core_v1.RecordReferrer: + """Export a Signature into a RecordReferrer.""" + return core_v1.RecordReferrer( + type=SIGNATURE_REFERRER_TYPE, + data=_marshal(signature), + ) + + +def unmarshal_signature(referrer: core_v1.RecordReferrer) -> sign_v1.Signature: + """Load a Signature from a RecordReferrer.""" + signature = sign_v1.Signature() + _unmarshal(referrer, signature) + return signature + + +def marshal_public_key(public_key: sign_v1.PublicKey) -> core_v1.RecordReferrer: + """Export a PublicKey into a RecordReferrer.""" + return core_v1.RecordReferrer( + type=PUBLIC_KEY_REFERRER_TYPE, + data=_marshal(public_key), + ) + + +def unmarshal_public_key(referrer: core_v1.RecordReferrer) -> sign_v1.PublicKey: + """Load a PublicKey from a RecordReferrer.""" + public_key = sign_v1.PublicKey() + _unmarshal(referrer, public_key) + return public_key diff --git a/dir-sdk-python/agntcy/dir_sdk/client/signing/sign.py b/dir-sdk-python/agntcy/dir_sdk/client/signing/sign.py new file mode 100644 index 0000000..2df29c6 --- /dev/null +++ b/dir-sdk-python/agntcy/dir_sdk/client/signing/sign.py @@ -0,0 +1,99 @@ +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +"""Native record signing (key and OIDC providers) and referrer publication.""" + +from __future__ import annotations + +import base64 +from datetime import datetime, timezone + +from agntcy.dir_sdk.client.services.store import StoreService +from agntcy.dir_sdk.client.signing import keys, oidc, referrer +from agntcy.dir_sdk.models import core_v1, sign_v1, store_v1 + + +def sign_blob_with_key( + payload: bytes, + req: sign_v1.SignWithKey, +) -> tuple[sign_v1.Signature, sign_v1.PublicKey]: + """Sign payload with a private key (inline PEM or key reference).""" + if not req.private_key: + msg = "private_key is required" + raise ValueError(msg) + + try: + private_key = keys.load_private_key(req.private_key, bytes(req.password)) + except keys.KeyLoadError as e: + msg = f"loading private key from reference: {e}" + raise ValueError(msg) from e + + raw_signature = keys.sign_message(private_key, payload) + public_key_pem = keys.public_key_pem(private_key) + + signature = sign_v1.Signature( + signed_at=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + signature=base64.b64encode(raw_signature).decode("ascii"), + algorithm=keys.detect_key_algorithm(public_key_pem), + ) + return signature, sign_v1.PublicKey(key=public_key_pem) + + +def sign_record(store_service: StoreService, req: sign_v1.SignRequest) -> None: + """Sign the record CID and push the public key and signature as referrers.""" + provider = req.provider.WhichOneof("request") if req.HasField("provider") else None + if provider is None: + msg = "signature provider must be specified" + raise ValueError(msg) + if not req.HasField("record_ref") or not req.record_ref.cid: + msg = "record ref must be specified" + raise ValueError(msg) + + payload = req.record_ref.cid.encode("utf-8") + try: + if provider == "key": + signature, public_key = sign_blob_with_key(payload, req.provider.key) + elif provider == "oidc": + signature, public_key = oidc.sign_blob_with_oidc(payload, req.provider.oidc) + else: + msg = f"unsupported signature provider type: {provider}" + raise ValueError(msg) + except (ValueError, oidc.OIDCError) as e: + msg = f"failed to sign record: {e}" + raise RuntimeError(msg) from e + + _push_referrers_to_store(store_service, req.record_ref, signature, public_key) + + +def _push_referrers_to_store( + store_service: StoreService, + record_ref: core_v1.RecordRef, + signature: sign_v1.Signature, + public_key: sign_v1.PublicKey, +) -> None: + public_key_referrer = referrer.marshal_public_key(public_key) + try: + store_service.push_referrer([_push_request(record_ref, public_key_referrer)]) + except Exception as e: + msg = f"failed to store public key: {e}" + raise RuntimeError(msg) from e + + signature_referrer = referrer.marshal_signature(signature) + try: + store_service.push_referrer([_push_request(record_ref, signature_referrer)]) + except Exception as e: + msg = f"failed to store signature: {e}" + raise RuntimeError(msg) from e + + +def _push_request( + record_ref: core_v1.RecordRef, + record_referrer: core_v1.RecordReferrer, +) -> store_v1.PushReferrerRequest: + return store_v1.PushReferrerRequest( + record_ref=record_ref, + type=record_referrer.type, + annotations=record_referrer.annotations, + created_at=record_referrer.created_at, + data=record_referrer.data, + ) diff --git a/dir-sdk-python/agntcy/dir_sdk/client/signing/verify.py b/dir-sdk-python/agntcy/dir_sdk/client/signing/verify.py new file mode 100644 index 0000000..eb2f0fd --- /dev/null +++ b/dir-sdk-python/agntcy/dir_sdk/client/signing/verify.py @@ -0,0 +1,239 @@ +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +"""Native record verification (VerifyWithFetcher semantics of the Go client).""" + +from __future__ import annotations + +import base64 +import logging +from collections.abc import Sequence + +from agntcy.dir_sdk.client.services.store import StoreService +from agntcy.dir_sdk.client.signing import keys, oidc, referrer +from agntcy.dir_sdk.models import core_v1, sign_v1, store_v1 + +logger = logging.getLogger("client") + +_RECORD_NOT_FOUND = "record not found" + + +def pull_signatures( + store_service: StoreService, + record_ref: core_v1.RecordRef, + metadata: Sequence[tuple[str, str]] | None = None, +) -> list[sign_v1.Signature]: + """Fetch all signature referrers for a record.""" + responses = store_service.pull_referrer( + [ + store_v1.PullReferrerRequest( + record_ref=record_ref, + referrer_type=referrer.SIGNATURE_REFERRER_TYPE, + ) + ], + metadata=metadata, + ) + signatures: list[sign_v1.Signature] = [] + for response in responses: + if not response.HasField("referrer"): + continue + try: + signatures.append(referrer.unmarshal_signature(response.referrer)) + except Exception as e: # noqa: BLE001 + logger.debug("Failed to unmarshal signature referrer: %s", e) + return signatures + + +def pull_public_keys( + store_service: StoreService, + record_ref: core_v1.RecordRef, + metadata: Sequence[tuple[str, str]] | None = None, +) -> list[str]: + """Fetch all public key referrers for a record.""" + responses = store_service.pull_referrer( + [ + store_v1.PullReferrerRequest( + record_ref=record_ref, + referrer_type=referrer.PUBLIC_KEY_REFERRER_TYPE, + ) + ], + metadata=metadata, + ) + public_keys: list[str] = [] + for response in responses: + if not response.HasField("referrer"): + continue + try: + public_key = referrer.unmarshal_public_key(response.referrer) + except Exception as e: # noqa: BLE001 + logger.debug("Failed to unmarshal public key referrer: %s", e) + continue + if public_key.key: + public_keys.append(public_key.key) + return public_keys + + +def _failure(message: str) -> sign_v1.VerifyResponse: + return sign_v1.VerifyResponse(success=False, error_message=message) + + +def _is_record_not_found(error: Exception) -> bool: + if _RECORD_NOT_FOUND in str(error): + return True + details = getattr(error, "details", None) + return callable(details) and _RECORD_NOT_FOUND in str(details()) + + +def verify_record( + store_service: StoreService, + req: sign_v1.VerifyRequest, + metadata: Sequence[tuple[str, str]] | None = None, +) -> sign_v1.VerifyResponse: + """Verify record signatures locally against the signatures held in the store.""" + if not req.HasField("record_ref") or not req.record_ref.cid: + msg = "record CID is required" + raise ValueError(msg) + + try: + store_service.lookup([req.record_ref], metadata=metadata) + except Exception as e: + if _is_record_not_found(e): + return _failure(_RECORD_NOT_FOUND) + msg = f"failed to lookup record: {e}" + raise RuntimeError(msg) from e + + return verify_with_fetcher(store_service, req, metadata=metadata) + + +def _verify_with_keys( + payload: bytes, + public_keys: Sequence[str], + signature: sign_v1.Signature, +) -> sign_v1.SignerInfo: + """Try every public key against the signature; first match wins.""" + for public_key in public_keys: + try: + key = keys.load_public_key(public_key) + except keys.KeyLoadError: + continue + try: + raw_signature = base64.b64decode(signature.signature, validate=True) + except ValueError: + # If decoding fails, assume it's already raw bytes. + raw_signature = signature.signature.encode("utf-8") + if not keys.verify_message(key, raw_signature, payload): + continue + pem = keys.public_key_pem(key) + return sign_v1.SignerInfo( + key=sign_v1.SignerInfoKey( + public_key=pem, + algorithm=keys.detect_key_algorithm(pem), + ) + ) + msg = "no valid signature found for the provided public keys" + raise ValueError(msg) + + +def _verify_with_any( + payload: bytes, + public_keys: Sequence[str], + signature: sign_v1.Signature, + oidc_options: sign_v1.VerifyOptionsOIDC, +) -> sign_v1.SignerInfo: + if not signature.content_bundle: + try: + return _verify_with_keys(payload, public_keys, signature) + except ValueError as e: + msg = f"failed to verify signature with keys: {e}" + raise ValueError(msg) from e + try: + return oidc.verify_with_oidc( + payload, sign_v1.VerifyWithOIDC(options=oidc_options), signature + ) + except oidc.OIDCError as e: + msg = f"failed to verify signature with OIDC: {e}" + raise ValueError(msg) from e + + +def _signer_key(signer: sign_v1.SignerInfo) -> str: + kind = signer.WhichOneof("type") + if kind == "key": + return f"key:{signer.key.SerializeToString(deterministic=True).hex()}" + if kind == "oidc": + return f"oidc:{signer.oidc.SerializeToString(deterministic=True).hex()}" + return "" + + +def verify_with_fetcher( + store_service: StoreService, + req: sign_v1.VerifyRequest, + metadata: Sequence[tuple[str, str]] | None = None, +) -> sign_v1.VerifyResponse: + """Verify all signatures of a record according to the request provider.""" + if not req.HasField("record_ref") or not req.record_ref.cid: + msg = "record ref is required" + raise ValueError(msg) + + provider_kind = ( + req.provider.WhichOneof("request") if req.HasField("provider") else None + ) + if provider_kind is None: + # Default provider: any signer, default OIDC options. + provider = sign_v1.VerifyRequestProvider(any=sign_v1.VerifyWithAny()) + provider_kind = "any" + else: + provider = req.provider + + try: + signatures = pull_signatures(store_service, req.record_ref, metadata) + except Exception as e: + msg = f"pull signatures: {e}" + raise RuntimeError(msg) from e + + if not signatures: + return _failure("no signatures found") + + public_keys: list[str] = [] + if provider_kind == "key": + public_keys = [provider.key.public_key] + elif provider_kind == "any": + try: + public_keys = pull_public_keys(store_service, req.record_ref, metadata) + except Exception as e: + msg = f"pull public keys: {e}" + raise RuntimeError(msg) from e + + payload = req.record_ref.cid.encode("utf-8") + oidc_options = ( + provider.any.oidc_options + if provider_kind == "any" and provider.any.HasField("oidc_options") + else sign_v1.VerifyOptionsOIDC() + ) + + seen_keys: set[str] = set() + signers: list[sign_v1.SignerInfo] = [] + for signature in signatures: + try: + if provider_kind == "oidc": + signer_info = oidc.verify_with_oidc(payload, provider.oidc, signature) + elif provider_kind == "key": + signer_info = _verify_with_keys(payload, public_keys, signature) + else: + signer_info = _verify_with_any( + payload, public_keys, signature, oidc_options + ) + except Exception as e: # noqa: BLE001 + # Failed verifications have no SignerInfo; only verified signers are reported. + logger.debug("Signature verification failed: %s", e) + continue + + signer_key = _signer_key(signer_info) + if signer_key in seen_keys: + continue # Only report one result per signer + seen_keys.add(signer_key) + signers.append(signer_info) + + if not signers: + return _failure("no valid signatures found matching verification criteria") + + return sign_v1.VerifyResponse(success=True, signers=signers) diff --git a/dir-sdk-python/agntcy/dir_sdk/client/transport/channels.py b/dir-sdk-python/agntcy/dir_sdk/client/transport/channels.py index ce45b00..a13e07b 100644 --- a/dir-sdk-python/agntcy/dir_sdk/client/transport/channels.py +++ b/dir-sdk-python/agntcy/dir_sdk/client/transport/channels.py @@ -15,7 +15,6 @@ JWTAuthInterceptor, ) from cryptography.hazmat.primitives import serialization -from spiffe import WorkloadApiClient, X509Source def grpc_channel_options(config: Config) -> list[tuple[str, str]]: @@ -51,6 +50,10 @@ def create_x509_channel(config: Config) -> grpc.Channel: msg = "SPIFFE socket path is required for X.509 authentication" raise ValueError(msg) + # Imported lazily: importing spiffe emits a cryptography FFDH deprecation + # warning, which should only affect users of SPIFFE authentication. + from spiffe import WorkloadApiClient, X509Source + workload_client = WorkloadApiClient(socket_path=config.spiffe_socket_path) x509_src = X509Source( workload_api_client=workload_client, @@ -90,6 +93,10 @@ def create_jwt_channel(config: Config) -> grpc.Channel: msg = "JWT audience is required for JWT authentication" raise ValueError(msg) + # Imported lazily: importing spiffe emits a cryptography FFDH deprecation + # warning, which should only affect users of SPIFFE authentication. + from spiffe import WorkloadApiClient, X509Source + workload_client = WorkloadApiClient(socket_path=config.spiffe_socket_path) x509_source = X509Source( workload_api_client=workload_client, diff --git a/dir-sdk-python/agntcy/dir_sdk/client/transport/interceptors.py b/dir-sdk-python/agntcy/dir_sdk/client/transport/interceptors.py index cc726f7..881ab81 100644 --- a/dir-sdk-python/agntcy/dir_sdk/client/transport/interceptors.py +++ b/dir-sdk-python/agntcy/dir_sdk/client/transport/interceptors.py @@ -9,7 +9,6 @@ import grpc from google.protobuf import message -from spiffe import WorkloadApiClient UnaryContinuation = Callable[[grpc.ClientCallDetails, message.Message], grpc.Call] StreamContinuation = Callable[ @@ -40,6 +39,10 @@ class JWTAuthInterceptor( """Add SPIFFE JWT-SVID authorization metadata to outgoing requests.""" def __init__(self, socket_path: str, audience: str) -> None: + # Imported lazily: importing spiffe emits a cryptography FFDH deprecation + # warning, which should only affect users of SPIFFE authentication. + from spiffe import WorkloadApiClient + self._audience = audience self._workload_client = WorkloadApiClient(socket_path=socket_path) diff --git a/dir-sdk-python/agntcy/dir_sdk/tests/test_client.py b/dir-sdk-python/agntcy/dir_sdk/tests/test_client.py index cc4a8f3..cdfd5a0 100644 --- a/dir-sdk-python/agntcy/dir_sdk/tests/test_client.py +++ b/dir-sdk-python/agntcy/dir_sdk/tests/test_client.py @@ -312,27 +312,13 @@ def test_sign_and_verify(self) -> None: shell_env = os.environ.copy() shell_env["COSIGN_PASSWORD"] = key_password - docker_config = self.client.config.docker_config - if docker_config: - docker_config.envs["COSIGN_PASSWORD"] = key_password - # Generate a key pair using cosign cosign_path = os.getenv("COSIGN_PATH", "cosign") command = (cosign_path, "generate-key-pair") subprocess.run(command, check=True, capture_output=True, env=shell_env) - if docker_config: - cosign_key_path = pathlib.Path("cosign.key").absolute() - cosign_pub_path = pathlib.Path("cosign.pub").absolute() - docker_config.mounts.append( - f"type=bind,src={cosign_key_path},dst=/cosign.key" - ) - docker_config.mounts.append( - f"type=bind,src={cosign_pub_path},dst=/cosign.pub" - ) - # Prepare Key signing request using file path reference - # The CLI will load the key from the file path directly + # The SDK loads the key from the file path directly key_provider = sign_v1.SignWithKey( private_key="cosign.key", password=key_password.encode("utf-8"), diff --git a/dir-sdk-python/agntcy/dir_sdk/tests/test_dirctl_interop.py b/dir-sdk-python/agntcy/dir_sdk/tests/test_dirctl_interop.py new file mode 100644 index 0000000..b60cbe2 --- /dev/null +++ b/dir-sdk-python/agntcy/dir_sdk/tests/test_dirctl_interop.py @@ -0,0 +1,343 @@ +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +"""Interoperability tests between the SDK and the dirctl CLI. + +Records signed by the SDK must verify with dirctl, and records signed by dirctl +must verify with the SDK. These tests need a running Directory server (see +DIRECTORY_CLIENT_SERVER_ADDRESS), the dirctl binary (DIRCTL_PATH or PATH) and +the cosign binary (COSIGN_PATH or PATH) and are skipped otherwise. +Only key-based signing is covered; keyless signing needs live Sigstore services. +""" + +from __future__ import annotations + +import base64 +import hashlib +import json +import os +import pathlib +import shutil +import subprocess +import tempfile +import unittest +import uuid +from typing import Any + +import grpc +from agntcy.dir_sdk.client import Client +from agntcy.dir_sdk.client.signing import referrer, verify +from agntcy.dir_sdk.models import core_v1, sign_v1, store_v1 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ed25519 +from nacl.secret import SecretBox + +KEY_PASSWORD = "interop-pw" +DIRCTL = os.getenv("DIRCTL_PATH") or shutil.which("dirctl") +COSIGN = os.getenv("COSIGN_PATH") or shutil.which("cosign") + + +def _write_cosign_ed25519_key(directory: pathlib.Path) -> tuple[str, str]: + """Write an Ed25519 key pair in the cosign encrypted format (cosign cannot).""" + key = ed25519.Ed25519PrivateKey.generate() + pkcs8 = key.private_bytes( + serialization.Encoding.DER, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) + salt, nonce = os.urandom(32), os.urandom(24) + derived = hashlib.scrypt( + KEY_PASSWORD.encode(), + salt=salt, + n=32768, + r=8, + p=1, + maxmem=64 * 1024 * 1024, + dklen=32, + ) + ciphertext = SecretBox(derived).encrypt(pkcs8, nonce).ciphertext + envelope = { + "kdf": { + "name": "scrypt", + "params": {"N": 32768, "r": 8, "p": 1}, + "salt": base64.b64encode(salt).decode(), + }, + "cipher": {"name": "nacl/secretbox", "nonce": base64.b64encode(nonce).decode()}, + "ciphertext": base64.b64encode(ciphertext).decode(), + } + body = base64.b64encode(json.dumps(envelope).encode()).decode() + lines = "\n".join(body[i : i + 64] for i in range(0, len(body), 64)) + private_path = directory / "ed25519.key" + private_path.write_text( + "-----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY-----\n" + f"{lines}\n-----END ENCRYPTED SIGSTORE PRIVATE KEY-----\n" + ) + public_path = directory / "ed25519.pub" + public_path.write_bytes( + key.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + ) + return str(private_path), str(public_path) + + +@unittest.skipUnless(DIRCTL and COSIGN, "dirctl and cosign binaries are required") +class TestDirctlInterop(unittest.TestCase): + client: Client + tmp: tempfile.TemporaryDirectory[str] + keys: dict[str, tuple[str, str]] + + @classmethod + def setUpClass(cls) -> None: + cls.client = Client() + try: + cls.client.push([cls._record("probe")]) + except grpc.RpcError as e: + msg = f"Directory server is not reachable: {e}" + raise unittest.SkipTest(msg) from e + + cls.tmp = tempfile.TemporaryDirectory() + directory = pathlib.Path(cls.tmp.name) + env = {**os.environ, "COSIGN_PASSWORD": KEY_PASSWORD} + subprocess.run( + [str(COSIGN), "generate-key-pair"], + cwd=directory, + env=env, + check=True, + capture_output=True, + ) + cls.keys = { + "ECDSA-P-256": ( + str(directory / "cosign.key"), + str(directory / "cosign.pub"), + ), + "Ed25519": _write_cosign_ed25519_key(directory), + } + + @classmethod + def tearDownClass(cls) -> None: + cls.tmp.cleanup() + + @staticmethod + def _record(name: str) -> core_v1.Record: + return core_v1.Record( + data={ + "name": f"agntcy-interop-{name}-{uuid.uuid4().hex[:8]}", + "version": "v3.0.0", + "schema_version": "0.7.0", + "description": "Record used to test dirctl and SDK interoperability.", + "authors": ["Cisco Systems"], + "created_at": "2025-03-19T17:06:37Z", + "skills": [ + { + "name": "natural_language_processing/natural_language_generation/text_completion", + "id": 10201, + } + ], + "locators": [ + { + "type": "docker_image", + "url": "https://ghcr.io/agntcy/marketing-strategy", + } + ], + "domains": [{"name": "technology/networking", "id": 103}], + "modules": [], + } + ) + + def _push(self, name: str) -> core_v1.RecordRef: + return self.client.push([self._record(name)])[0] + + @staticmethod + def _dirctl(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]: + env = { + **os.environ, + "COSIGN_PASSWORD": KEY_PASSWORD, + "DIRECTORY_CLIENT_SERVER_ADDRESS": os.getenv( + "DIRECTORY_CLIENT_SERVER_ADDRESS", "0.0.0.0:8888" + ), + } + return subprocess.run( + [str(DIRCTL), *args], + check=check, + capture_output=True, + text=True, + env=env, + timeout=120, + ) + + def _dirctl_verify(self, cid: str, public_key: str | None = None) -> dict[str, Any]: + args = ["verify", cid, "--output", "json"] + if public_key: + args += ["--key", public_key] + result = self._dirctl(*args, check=False) + assert result.returncode == 0, result.stderr + return json.loads(result.stdout) + + def _sdk_sign(self, ref: core_v1.RecordRef, private_key: str) -> None: + self.client.sign( + sign_v1.SignRequest( + record_ref=ref, + provider=sign_v1.SignRequestProvider( + key=sign_v1.SignWithKey( + private_key=private_key, password=KEY_PASSWORD.encode() + ) + ), + ) + ) + + def _sdk_verify( + self, ref: core_v1.RecordRef, public_key: str | None = None + ) -> sign_v1.VerifyResponse: + provider = ( + sign_v1.VerifyRequestProvider( + key=sign_v1.VerifyWithKey(public_key=public_key) + ) + if public_key + else sign_v1.VerifyRequestProvider(any=sign_v1.VerifyWithAny()) + ) + return self.client.verify( + sign_v1.VerifyRequest(record_ref=ref, provider=provider) + ) + + def _public_pem(self, algorithm: str) -> str: + return pathlib.Path(self.keys[algorithm][1]).read_text() + + def test_sdk_signature_verifies_with_dirctl(self) -> None: + for algorithm, (private_key, public_key) in self.keys.items(): + with self.subTest(algorithm): + ref = self._push(f"sdk-{algorithm}") + self._sdk_sign(ref, private_key) + + for result in ( + self._dirctl_verify(ref.cid), + self._dirctl_verify(ref.cid, public_key), + ): + assert result["success"], result + signer = result["signers"][0]["key"] + assert signer["algorithm"] == algorithm + assert signer["publicKey"] == self._public_pem(algorithm) + + def test_dirctl_signature_verifies_with_sdk(self) -> None: + for algorithm, (private_key, public_key) in self.keys.items(): + with self.subTest(algorithm): + ref = self._push(f"dirctl-{algorithm}") + self._dirctl("sign", ref.cid, "--key", private_key) + + for response in ( + self._sdk_verify(ref), + self._sdk_verify(ref, public_key), + ): + assert response.success, response.error_message + signer = response.signers[0].key + assert signer.algorithm == algorithm + assert signer.public_key == self._public_pem(algorithm) + + def test_sdk_and_dirctl_produce_equivalent_referrers(self) -> None: + for algorithm, (private_key, _) in self.keys.items(): + with self.subTest(algorithm): + sdk_ref = self._push(f"equiv-sdk-{algorithm}") + dirctl_ref = self._push(f"equiv-dirctl-{algorithm}") + self._sdk_sign(sdk_ref, private_key) + self._dirctl("sign", dirctl_ref.cid, "--key", private_key) + + sdk_sigs = self._pull_signatures(sdk_ref) + dirctl_sigs = self._pull_signatures(dirctl_ref) + assert len(sdk_sigs) == len(dirctl_sigs) == 1 + sdk_sig, dirctl_sig = sdk_sigs[0], dirctl_sigs[0] + + # Same populated fields and metadata; only the values that depend + # on time and (for ECDSA) randomness may differ. + populated = [ + {field.name for field, _ in sig.ListFields()} + for sig in (sdk_sig, dirctl_sig) + ] + assert populated[0] == populated[1] + assert sdk_sig.algorithm == dirctl_sig.algorithm == algorithm + assert sdk_sig.content_bundle == dirctl_sig.content_bundle == "" + assert self._pull_public_keys(sdk_ref) == self._pull_public_keys( + dirctl_ref + ) + + def test_signature_is_rejected_for_a_different_record(self) -> None: + """A signature must only be valid for the CID it was created for.""" + private_key, public_key = self.keys["ECDSA-P-256"] + signed = self._push("payload-signed") + other = self._push("payload-other") + self._sdk_sign(signed, private_key) + + # Re-attach the SDK's referrers to a different record. + for sig in self._pull_signatures(signed): + self._attach(other, sig) + self._attach(other, sign_v1.PublicKey(key=self._public_pem("ECDSA-P-256"))) + assert not self._sdk_verify(other, public_key).success + assert not self._dirctl_verify(other.cid, public_key).get("success", False) + + def test_wrong_key_is_rejected_by_both(self) -> None: + private_key, _ = self.keys["ECDSA-P-256"] + _, other_public = self.keys["Ed25519"] + + sdk_ref = self._push("wrong-key-sdk") + self._sdk_sign(sdk_ref, private_key) + dirctl_ref = self._push("wrong-key-dirctl") + self._dirctl("sign", dirctl_ref.cid, "--key", private_key) + + for ref in (sdk_ref, dirctl_ref): + assert not self._sdk_verify(ref, other_public).success + assert not self._dirctl_verify(ref.cid, other_public).get("success", False) + + def test_cosign_verifies_sdk_signature(self) -> None: + private_key, public_key = self.keys["ECDSA-P-256"] + ref = self._push("cosign") + self._sdk_sign(ref, private_key) + signature = self._pull_signatures(ref)[0].signature + + payload = pathlib.Path(self.tmp.name) / "payload.txt" + payload.write_bytes(ref.cid.encode()) + subprocess.run( + [ + str(COSIGN), + "verify-blob", + "--key", + public_key, + "--signature", + signature, + "--insecure-ignore-tlog", + str(payload), + ], + check=True, + capture_output=True, + ) + + def _pull_signatures(self, ref: core_v1.RecordRef) -> list[sign_v1.Signature]: + return verify.pull_signatures(self.client.store_service, ref) + + def _pull_public_keys(self, ref: core_v1.RecordRef) -> list[str]: + return verify.pull_public_keys(self.client.store_service, ref) + + def _attach( + self, + ref: core_v1.RecordRef, + message: sign_v1.Signature | sign_v1.PublicKey, + ) -> None: + record_referrer = ( + referrer.marshal_signature(message) + if isinstance(message, sign_v1.Signature) + else referrer.marshal_public_key(message) + ) + self.client.push_referrer( + [ + store_v1.PushReferrerRequest( + record_ref=ref, + type=record_referrer.type, + annotations=record_referrer.annotations, + created_at=record_referrer.created_at, + data=record_referrer.data, + ) + ] + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/dir-sdk-python/agntcy/dir_sdk/tests/test_signing.py b/dir-sdk-python/agntcy/dir_sdk/tests/test_signing.py new file mode 100644 index 0000000..2b90926 --- /dev/null +++ b/dir-sdk-python/agntcy/dir_sdk/tests/test_signing.py @@ -0,0 +1,1559 @@ +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +"""Offline unit tests for native signing and verification.""" + +from __future__ import annotations + +import base64 +import hashlib +import json +import os +import pathlib +import tempfile +import unittest +import unittest.mock +from collections.abc import Sequence +from datetime import datetime, timedelta, timezone +from types import SimpleNamespace +from typing import Any + +from agntcy.dir_sdk.client.services.store import StoreService +from agntcy.dir_sdk.client.signing import ed25519ph, keys, oidc, referrer +from agntcy.dir_sdk.client.signing import verify as verify_mod +from agntcy.dir_sdk.client.signing.sign import sign_blob_with_key, sign_record +from agntcy.dir_sdk.client.signing.verify import verify_record +from agntcy.dir_sdk.models import core_v1, sign_v1, store_v1 +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa, x25519 +from cryptography.hazmat.primitives.asymmetric.utils import Prehashed +from nacl.secret import SecretBox + +CID = "bafkreibtestcidforsigning" + +# Key pair generated with `cosign generate-key-pair` (password: "pw"). +COSIGN_KEY = """-----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY----- +eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjo2NTUzNiwiciI6 +OCwicCI6MX0sInNhbHQiOiJNRWc4VThsVWZiTnZCbXd2UWZiMjVOcFZWY01VME9Z +Rm43c0o5L1FtN0RZPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94 +Iiwibm9uY2UiOiJsRjQvNWNpc0w3TWRJREVLbm52Wm91UEgrTit5cG1waiJ9LCJj +aXBoZXJ0ZXh0Ijoid2hKc09qZGFmdHJ6eGRBUk1PbCtJY3JGbC9Gc056Z3dQaGJU +OXpwZStLSnJZNUk0SHNTNHUrMjhDVDZYeXF6VDFDQWlvM3RkeGQ2K1JUN1pwRTVV +K2h6Z2ZmZnA4M2tOZnpscENpZ1NYSEVhK2poQlE5dUxZSk40d1IrRFRiaHFoc2dj +T2VFdkoxaG5HLy9CNm9EeDJkMmVITkw4c0ltSnk5OTY3aDZRQ2pJRkdnK0xOTmM3 +QlBuRG5YU0N0aWwyaFNkZis1a2NSVmNHZFE9PSJ9 +-----END ENCRYPTED SIGSTORE PRIVATE KEY----- +""" +COSIGN_PUB = """-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEut6/BVok2fw1ILDkX10HQxmriTAS +I5pw7NLDC6Bi3FKQl1FkOmVubQSbaj07C8LP1E2yDqOM1buTnz33HGiCEg== +-----END PUBLIC KEY----- +""" +# `cosign sign-blob --key` over the payload b"bafytestcid". +COSIGN_BLOB_SIGNATURE = ( + "MEUCIA8RuVpvLWX1s5Nr2JoPZOBwc+WYxUb1YqPttPKcA4OdAiEA8uOpbZkvcqc/" + "sZUsqGWNAESLuDVzJN6jmJj06EDA1ts=" +) + + +def _pem_private(key: object, password: bytes | None = None) -> str: + encryption = ( + serialization.BestAvailableEncryption(password) + if password + else serialization.NoEncryption() + ) + return key.private_bytes( # type: ignore[attr-defined] + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + encryption, + ).decode() + + +def _cosign_encrypt(key: object, password: bytes) -> str: + """Encode a key in the cosign encrypted-key format (small scrypt cost).""" + pkcs8 = key.private_bytes( # type: ignore[attr-defined] + serialization.Encoding.DER, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) + salt, nonce = os.urandom(32), os.urandom(24) + derived = hashlib.scrypt(password, salt=salt, n=1024, r=8, p=1, dklen=32) + ciphertext = SecretBox(derived).encrypt(pkcs8, nonce).ciphertext + envelope = { + "kdf": { + "name": "scrypt", + "params": {"N": 1024, "r": 8, "p": 1}, + "salt": base64.b64encode(salt).decode(), + }, + "cipher": {"name": "nacl/secretbox", "nonce": base64.b64encode(nonce).decode()}, + "ciphertext": base64.b64encode(ciphertext).decode(), + } + body = base64.b64encode(json.dumps(envelope).encode()).decode() + lines = [body[i : i + 64] for i in range(0, len(body), 64)] + return "\n".join( + [ + "-----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY-----", + *lines, + "-----END ENCRYPTED SIGSTORE PRIVATE KEY-----", + "", + ] + ) + + +def _make_ca() -> tuple[ec.EllipticCurvePrivateKey, x509.Certificate]: + now = datetime.now(timezone.utc) + ca_key = ec.generate_private_key(ec.SECP256R1()) + ca_name = x509.Name( + [x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "test-fulcio")] + ) + ca_cert = ( + x509.CertificateBuilder() + .subject_name(ca_name) + .issuer_name(ca_name) + .public_key(ca_key.public_key()) + .serial_number(1) + .not_valid_before(now - timedelta(days=1)) + .not_valid_after(now + timedelta(days=1)) + .add_extension(x509.BasicConstraints(ca=True, path_length=None), True) + .add_extension( + x509.KeyUsage(False, False, False, False, False, True, True, False, False), + True, + ) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()), False + ) + .sign(ca_key, hashes.SHA256()) + ) + return ca_key, ca_cert + + +def _issue_leaf( + ca_key: ec.EllipticCurvePrivateKey, + ca_cert: x509.Certificate, + public_key: ec.EllipticCurvePublicKey, + subject: str, +) -> x509.Certificate: + now = datetime.now(timezone.utc) + issuer = b"https://issuer.example" + return ( + x509.CertificateBuilder() + .subject_name(x509.Name([])) + .issuer_name(ca_cert.subject) + .public_key(public_key) + .serial_number(2) + .not_valid_before(now - timedelta(minutes=1)) + .not_valid_after(now + timedelta(minutes=10)) + .add_extension(x509.BasicConstraints(ca=False, path_length=None), True) + .add_extension( + x509.KeyUsage(True, False, False, False, False, False, False, False, False), + True, + ) + .add_extension( + x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.CODE_SIGNING]), False + ) + .add_extension( + x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()), + False, + ) + .add_extension( + x509.SubjectAlternativeName([x509.UniformResourceIdentifier(subject)]), + True, + ) + .add_extension( + x509.UnrecognizedExtension( + oidc._OID_ISSUER_V2, b"\x0c" + bytes([len(issuer)]) + issuer + ), + False, + ) + .sign(ca_key, hashes.SHA256()) + ) + + +class FakeStore(StoreService): + """In-memory stand-in for the store referrer and lookup RPCs.""" + + def __init__(self, known_cids: Sequence[str] = (CID,)) -> None: + self.known_cids = set(known_cids) + self.pushed: list[store_v1.PushReferrerRequest] = [] + + def push_referrer( + self, + req: list[store_v1.PushReferrerRequest], + metadata: Sequence[tuple[str, str]] | None = None, # noqa: ARG002 + ) -> list[store_v1.PushReferrerResponse]: + self.pushed.extend(req) + return [store_v1.PushReferrerResponse(success=True) for _ in req] + + def pull_referrer( + self, + req: list[store_v1.PullReferrerRequest], + metadata: Sequence[tuple[str, str]] | None = None, # noqa: ARG002 + ) -> list[store_v1.PullReferrerResponse]: + out = [] + for r in req: + for pushed in self.pushed: + if ( + pushed.record_ref.cid == r.record_ref.cid + and pushed.type == r.referrer_type + ): + out.append( + store_v1.PullReferrerResponse( + referrer=core_v1.RecordReferrer( + type=pushed.type, data=pushed.data + ) + ) + ) + return out + + def lookup( + self, + refs: list[core_v1.RecordRef], + metadata: Sequence[tuple[str, str]] | None = None, # noqa: ARG002 + ) -> list[core_v1.RecordMeta]: + for ref in refs: + if ref.cid not in self.known_cids: + msg = "record not found" + raise RuntimeError(msg) + return [core_v1.RecordMeta(cid=ref.cid) for ref in refs] + + +def _key_sign_request(private_key: str, password: bytes = b"") -> sign_v1.SignRequest: + return sign_v1.SignRequest( + record_ref=core_v1.RecordRef(cid=CID), + provider=sign_v1.SignRequestProvider( + key=sign_v1.SignWithKey(private_key=private_key, password=password) + ), + ) + + +class TestEd25519ph(unittest.TestCase): + def test_rfc8032_vector(self) -> None: + seed = bytes.fromhex( + "833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42" + ) + pub = bytes.fromhex( + "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf" + ) + expected = bytes.fromhex( + "98a70222f0b8121aa9d30f813d683f809e462b469c7ff87639499bb94e6dae41" + "31f85042463c2a355a2003d062adf5aaa10b8c61e636062aaad11c2a26083406" + ) + assert ed25519ph.public_key(seed) == pub + assert ed25519ph.sign(seed, b"abc") == expected + assert ed25519ph.verify(pub, b"abc", expected) + assert not ed25519ph.verify(pub, b"abd", expected) + + +class TestKeys(unittest.TestCase): + def test_roundtrip_plain_and_encrypted(self) -> None: + cases = { + "ECDSA-P-256": ec.generate_private_key(ec.SECP256R1()), + "ECDSA-P-384": ec.generate_private_key(ec.SECP384R1()), + "RSA-2048": rsa.generate_private_key(65537, 2048), + "Ed25519": ed25519.Ed25519PrivateKey.generate(), + } + for algorithm, key in cases.items(): + for pem, password in ( + (_pem_private(key), b""), + (_pem_private(key, b"secret"), b"secret"), + (_cosign_encrypt(key, b"secret"), b"secret"), + ): + with self.subTest(algorithm=algorithm, encrypted=bool(password)): + signature, public_key = sign_blob_with_key( + CID.encode(), + sign_v1.SignWithKey(private_key=pem, password=password), + ) + assert signature.algorithm == algorithm + assert signature.signed_at.endswith("Z") + loaded = keys.load_public_key(public_key.key) + raw = base64.b64decode(signature.signature) + assert keys.verify_message(loaded, raw, CID.encode()) + assert not keys.verify_message(loaded, raw, b"other") + + def test_ed25519_signs_as_ph(self) -> None: + key = ed25519.Ed25519PrivateKey.generate() + signature, _ = sign_blob_with_key( + CID.encode(), sign_v1.SignWithKey(private_key=_pem_private(key)) + ) + raw = base64.b64decode(signature.signature) + # A pure Ed25519 verifier must reject it; only the ph fallback accepts. + pub = key.public_key() + with self.assertRaises(Exception): # noqa: B017 + pub.verify(raw, CID.encode()) + assert keys.verify_message(pub, raw, CID.encode()) + + def test_key_file_reference(self) -> None: + key = ec.generate_private_key(ec.SECP256R1()) + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / "cosign.key" + path.write_text(_cosign_encrypt(key, b"pw")) + signature, public_key = sign_blob_with_key( + CID.encode(), sign_v1.SignWithKey(private_key=str(path), password=b"pw") + ) + pub_path = pathlib.Path(tmp) / "cosign.pub" + pub_path.write_text(public_key.key) + loaded = keys.load_public_key(str(pub_path)) + assert keys.verify_message( + loaded, base64.b64decode(signature.signature), CID.encode() + ) + + def test_wrong_password_and_missing_key(self) -> None: + pem = _cosign_encrypt(ec.generate_private_key(ec.SECP256R1()), b"right") + with self.assertRaises(ValueError): + sign_blob_with_key( + CID.encode(), sign_v1.SignWithKey(private_key=pem, password=b"wrong") + ) + with self.assertRaises(ValueError): + sign_blob_with_key(CID.encode(), sign_v1.SignWithKey(private_key="/nope")) + with self.assertRaises(ValueError): + sign_blob_with_key(CID.encode(), sign_v1.SignWithKey()) + + def test_real_cosign_key_and_signature(self) -> None: + signature, public_key = sign_blob_with_key( + b"bafytestcid", + sign_v1.SignWithKey(private_key=COSIGN_KEY, password=b"pw"), + ) + assert public_key.key == COSIGN_PUB + assert signature.algorithm == "ECDSA-P-256" + pub = keys.load_public_key(COSIGN_PUB) + # Signature produced by the cosign CLI verifies with our verifier. + assert keys.verify_message( + pub, base64.b64decode(COSIGN_BLOB_SIGNATURE), b"bafytestcid" + ) + # And ours verifies with the same verifier path. + assert keys.verify_message( + pub, base64.b64decode(signature.signature), b"bafytestcid" + ) + + +class TestReferrerEncoding(unittest.TestCase): + def test_signature_referrer(self) -> None: + signature = sign_v1.Signature( + signature="c2ln", + algorithm="ECDSA-P-256", + signed_at="2026-01-02T03:04:05Z", + content_type="application/vnd.dev.sigstore.bundle.v0.3+json", + content_bundle='{"a":1}', + certificate="Y2VydA==", + ) + ref = referrer.marshal_signature(signature) + assert ref.type == "agntcy.dir.sign.v1.Signature" + assert dict(ref.data.fields.items()).keys() == { + "signature", + "algorithm", + "signedAt", + "contentType", + "contentBundle", + "certificate", + } + assert ref.data["signedAt"] == "2026-01-02T03:04:05Z" + assert referrer.unmarshal_signature(ref) == signature + + def test_signature_referrer_omits_empty_fields(self) -> None: + ref = referrer.marshal_signature(sign_v1.Signature(signature="c2ln")) + assert list(ref.data.fields.keys()) == ["signature"] + + def test_public_key_referrer(self) -> None: + public_key = sign_v1.PublicKey(key=COSIGN_PUB) + ref = referrer.marshal_public_key(public_key) + assert ref.type == "agntcy.dir.sign.v1.PublicKey" + assert list(ref.data.fields.keys()) == ["key"] + assert referrer.unmarshal_public_key(ref) == public_key + + def test_unmarshal_requires_data(self) -> None: + with self.assertRaises(ValueError): + referrer.unmarshal_signature(core_v1.RecordReferrer()) + + +class TestSignAndVerify(unittest.TestCase): + def _signed_store(self) -> FakeStore: + store = FakeStore() + sign_record(store, _key_sign_request(COSIGN_KEY, b"pw")) + return store + + def test_sign_pushes_public_key_then_signature(self) -> None: + store = self._signed_store() + assert [p.type for p in store.pushed] == [ + "agntcy.dir.sign.v1.PublicKey", + "agntcy.dir.sign.v1.Signature", + ] + assert all(p.record_ref.cid == CID for p in store.pushed) + signature = referrer.unmarshal_signature( + core_v1.RecordReferrer(data=store.pushed[1].data) + ) + assert not signature.content_bundle + assert signature.algorithm == "ECDSA-P-256" + + def test_verify_default_any_key_and_dedupe(self) -> None: + store = self._signed_store() + sign_record(store, _key_sign_request(COSIGN_KEY, b"pw")) # same signer again + response = verify_record( + store, sign_v1.VerifyRequest(record_ref=core_v1.RecordRef(cid=CID)) + ) + assert response.success + assert len(response.signers) == 1 + assert response.signers[0].key.public_key == COSIGN_PUB + assert response.signers[0].key.algorithm == "ECDSA-P-256" + + def test_verify_with_key_provider(self) -> None: + store = self._signed_store() + ok = verify_record( + store, + sign_v1.VerifyRequest( + record_ref=core_v1.RecordRef(cid=CID), + provider=sign_v1.VerifyRequestProvider( + key=sign_v1.VerifyWithKey(public_key=COSIGN_PUB) + ), + ), + ) + assert ok.success + other = ec.generate_private_key(ec.SECP256R1()).public_key() + other_pem = other.public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo + ).decode() + bad = verify_record( + store, + sign_v1.VerifyRequest( + record_ref=core_v1.RecordRef(cid=CID), + provider=sign_v1.VerifyRequestProvider( + key=sign_v1.VerifyWithKey(public_key=other_pem) + ), + ), + ) + assert not bad.success + assert ( + bad.error_message + == "no valid signatures found matching verification criteria" + ) + + def test_verify_public_key_file_reference(self) -> None: + store = self._signed_store() + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / "cosign.pub" + path.write_text(COSIGN_PUB) + response = verify_record( + store, + sign_v1.VerifyRequest( + record_ref=core_v1.RecordRef(cid=CID), + provider=sign_v1.VerifyRequestProvider( + key=sign_v1.VerifyWithKey(public_key=str(path)) + ), + ), + ) + assert response.success + + def test_verify_no_signatures_and_record_not_found(self) -> None: + empty = FakeStore() + response = verify_record( + empty, sign_v1.VerifyRequest(record_ref=core_v1.RecordRef(cid=CID)) + ) + assert not response.success + assert response.error_message == "no signatures found" + + missing = verify_record( + empty, sign_v1.VerifyRequest(record_ref=core_v1.RecordRef(cid="unknown")) + ) + assert not missing.success + assert missing.error_message == "record not found" + + def test_verify_requires_cid(self) -> None: + with self.assertRaises(ValueError): + verify_record(FakeStore(), sign_v1.VerifyRequest()) + + def test_signature_for_other_record_is_rejected(self) -> None: + store = FakeStore(known_cids=(CID, "other")) + sign_record(store, _key_sign_request(COSIGN_KEY, b"pw")) + # Re-attach CID's signature referrers to another record. + for pushed in list(store.pushed): + moved = store_v1.PushReferrerRequest() + moved.CopyFrom(pushed) + moved.record_ref.cid = "other" + store.pushed.append(moved) + response = verify_record( + store, sign_v1.VerifyRequest(record_ref=core_v1.RecordRef(cid="other")) + ) + assert not response.success + + def test_sign_validation(self) -> None: + store = FakeStore() + with self.assertRaises(ValueError): + sign_record( + store, sign_v1.SignRequest(record_ref=core_v1.RecordRef(cid=CID)) + ) + with self.assertRaises(ValueError): + sign_record( + store, + sign_v1.SignRequest( + provider=sign_v1.SignRequestProvider( + key=sign_v1.SignWithKey(private_key=COSIGN_KEY) + ) + ), + ) + with self.assertRaises(RuntimeError): + sign_record(store, _key_sign_request("/nope")) + + +class TestOIDC(unittest.TestCase): + def test_value_matcher(self) -> None: + assert oidc.get_value_matcher("") == ("", ".*") + assert oidc.get_value_matcher("https://github.com/") == ( + "", + "https://github.com/", + ) + assert oidc.get_value_matcher("(unclosed") == ("(unclosed", "") + + def test_summarize_certificate(self) -> None: + key = ec.generate_private_key(ec.SECP256R1()) + name = x509.Name( + [x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "sigstore-intermediate")] + ) + issuer = b"https://issuer.example" + now = datetime.now(timezone.utc) + cert = ( + x509.CertificateBuilder() + .subject_name(x509.Name([])) + .issuer_name(name) + .public_key(key.public_key()) + .serial_number(1) + .not_valid_before(now) + .not_valid_after(now + timedelta(minutes=10)) + .add_extension( + x509.SubjectAlternativeName( + [x509.UniformResourceIdentifier("https://example.com/wf")] + ), + critical=False, + ) + .add_extension( + x509.UnrecognizedExtension( + oidc._OID_ISSUER_V2, + b"\x0c" + bytes([len(issuer)]) + issuer, + ), + critical=False, + ) + .sign(key, hashes.SHA256()) + ) + summary = oidc._summarize_certificate(cert) + assert summary.issuer == "https://issuer.example" + assert summary.subject == "https://example.com/wf" + assert summary.certificate_issuer == "CN=sigstore-intermediate" + + def _synthetic_signature( + self, tmp: str, subject: str = "https://example.com/wf@refs/heads/main" + ) -> tuple[sign_v1.Signature, str]: + """Create a bundle signed by a throwaway CA and the matching trusted root.""" + ca_key, ca_cert = _make_ca() + leaf_key = ec.generate_private_key(ec.SECP256R1()) + leaf = _issue_leaf(ca_key, ca_cert, leaf_key.public_key(), subject) + + raw_signature = leaf_key.sign(CID.encode(), ec.ECDSA(hashes.SHA256())) + digest = hashlib.sha256(CID.encode()).digest() + bundle = { + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": base64.b64encode( + leaf.public_bytes(serialization.Encoding.DER) + ).decode() + }, + "tlogEntries": [], + }, + "messageSignature": { + "messageDigest": { + "algorithm": "SHA2_256", + "digest": base64.b64encode(digest).decode(), + }, + "signature": base64.b64encode(raw_signature).decode(), + }, + } + trusted_root = { + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [], + "certificateAuthorities": [ + { + "subject": {"organization": "test", "commonName": "test-fulcio"}, + "uri": "https://fulcio.test", + "certChain": { + "certificates": [ + { + "rawBytes": base64.b64encode( + ca_cert.public_bytes(serialization.Encoding.DER) + ).decode() + } + ] + }, + "validFor": {"start": "2020-01-01T00:00:00Z"}, + } + ], + "ctlogs": [], + "timestampAuthorities": [], + } + root_path = pathlib.Path(tmp) / "trusted_root.json" + root_path.write_text(json.dumps(trusted_root)) + signature = sign_v1.Signature( + signature=base64.b64encode(raw_signature).decode(), + content_bundle=json.dumps(bundle), + content_type="application/vnd.dev.sigstore.bundle.v0.3+json", + ) + return signature, str(root_path) + + def _verify_synthetic( + self, + signature: sign_v1.Signature, + root_path: str, + *, + issuer: str = "", + subject: str = "", + payload: bytes = CID.encode(), + ignore_tlog: bool = False, + ignore_tsa: bool = False, + ignore_sct: bool = False, + ) -> sign_v1.SignerInfo: + return oidc.verify_with_oidc( + payload, + sign_v1.VerifyWithOIDC( + issuer=issuer, + subject=subject, + options=sign_v1.VerifyOptionsOIDC( + trusted_root_path=root_path, + ignore_tlog=ignore_tlog, + ignore_tsa=ignore_tsa, + ignore_sct=ignore_sct, + ), + ), + signature, + ) + + def test_verify_bundle_with_local_trusted_root(self) -> None: + relaxed: dict[str, Any] = { + "ignore_tlog": True, + "ignore_tsa": True, + "ignore_sct": True, + } + with tempfile.TemporaryDirectory() as tmp: + signature, root_path = self._synthetic_signature(tmp) + + info = self._verify_synthetic(signature, root_path, **relaxed) + assert info.oidc.issuer == "https://issuer.example" + assert info.oidc.subject == "https://example.com/wf@refs/heads/main" + assert info.oidc.certificate_issuer == "CN=test-fulcio" + + # Exact and regular expression identity matchers. + self._verify_synthetic( + signature, + root_path, + issuer="https://issuer.example", + subject="https://example.com/wf@refs/heads/main", + **relaxed, + ) + self._verify_synthetic( + signature, root_path, subject=r"^https://example\.com/.*", **relaxed + ) + + # Mismatching identity, payload or trust anchor are rejected. + with self.assertRaises(oidc.OIDCError): + self._verify_synthetic( + signature, root_path, issuer="https://other.example", **relaxed + ) + with self.assertRaises(oidc.OIDCError): + self._verify_synthetic( + signature, root_path, subject="https://evil.example/.*", **relaxed + ) + with self.assertRaises(oidc.OIDCError): + self._verify_synthetic( + signature, root_path, payload=b"other-cid", **relaxed + ) + _, other_root = self._synthetic_signature(tmp) + with self.assertRaises(oidc.OIDCError): + self._verify_synthetic(signature, other_root, **relaxed) + + # Without ignoring them, missing timestamps/log/SCT are rejected. + stricter: tuple[dict[str, Any], ...] = ( + {"ignore_tlog": True, "ignore_sct": True}, # TSA required + {"ignore_tsa": True, "ignore_sct": True}, # tlog required + {"ignore_tlog": True, "ignore_tsa": True}, # SCT required + ) + for strict in stricter: + with self.assertRaises(oidc.OIDCError): + self._verify_synthetic(signature, root_path, **strict) + + def test_verify_with_any_and_oidc_provider_through_store(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + signature, root_path = self._synthetic_signature(tmp) + relaxed = sign_v1.VerifyOptionsOIDC( + trusted_root_path=root_path, + ignore_tlog=True, + ignore_tsa=True, + ignore_sct=True, + ) + store = FakeStore() + store.pushed.append( + store_v1.PushReferrerRequest( + record_ref=core_v1.RecordRef(cid=CID), + type=referrer.SIGNATURE_REFERRER_TYPE, + data=referrer.marshal_signature(signature).data, + ) + ) + for provider in ( + sign_v1.VerifyRequestProvider( + any=sign_v1.VerifyWithAny(oidc_options=relaxed) + ), + sign_v1.VerifyRequestProvider( + oidc=sign_v1.VerifyWithOIDC( + issuer="https://issuer.example", options=relaxed + ) + ), + ): + response = verify_record( + store, + sign_v1.VerifyRequest( + record_ref=core_v1.RecordRef(cid=CID), provider=provider + ), + ) + assert response.success, response.error_message + assert response.signers[0].oidc.issuer == "https://issuer.example" + + # An identity mismatch is reported as a failed verification. + denied = verify_record( + store, + sign_v1.VerifyRequest( + record_ref=core_v1.RecordRef(cid=CID), + provider=sign_v1.VerifyRequestProvider( + oidc=sign_v1.VerifyWithOIDC( + issuer="https://other.example", options=relaxed + ) + ), + ), + ) + assert not denied.success + + def test_sign_with_oidc_produces_verifiable_bundle(self) -> None: + ca_key, ca_cert = _make_ca() + + class FakeFulcio: + def __init__(self, url: str) -> None: + self.url = url + self.signing_cert = self + + def post( + self, csr: x509.CertificateSigningRequest, _identity: object + ) -> SimpleNamespace: + public_key = csr.public_key() + assert isinstance(public_key, ec.EllipticCurvePublicKey) + return SimpleNamespace( + cert=_issue_leaf( + ca_key, ca_cert, public_key, "https://example.com/id" + ) + ) + + class FakeTSA: + def __init__(self, url: str) -> None: + self.url = url + + def request_timestamp(self, _signature: bytes) -> SimpleNamespace: + return SimpleNamespace(as_bytes=lambda: b"fake-timestamp") + + with ( + tempfile.TemporaryDirectory() as tmp, + unittest.mock.patch.object(oidc, "FulcioClient", FakeFulcio), + unittest.mock.patch.object(oidc, "TimestampAuthorityClient", FakeTSA), + unittest.mock.patch.object(oidc, "IdentityToken", lambda token, **_: token), + ): + signature, public_key = oidc.sign_blob_with_oidc( + CID.encode(), + sign_v1.SignWithOIDC( + id_token="token", + options=sign_v1.SignOptionsOIDC(skip_tlog=True), + ), + ) + assert signature.content_type.startswith( + "application/vnd.dev.sigstore.bundle" + ) + assert signature.signed_at.endswith("Z") + assert signature.certificate + assert public_key.key.startswith("-----BEGIN PUBLIC KEY-----") + bundle = json.loads(signature.content_bundle) + assert bundle["verificationMaterial"]["certificate"]["rawBytes"] == ( + signature.certificate + ) + assert bundle["messageSignature"]["signature"] == signature.signature + + root_path = pathlib.Path(tmp) / "trusted_root.json" + root_path.write_text( + json.dumps( + { + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [], + "certificateAuthorities": [ + { + "subject": {"organization": "t", "commonName": "t"}, + "uri": "https://fulcio.test", + "certChain": { + "certificates": [ + { + "rawBytes": base64.b64encode( + ca_cert.public_bytes( + serialization.Encoding.DER + ) + ).decode() + } + ] + }, + "validFor": {"start": "2020-01-01T00:00:00Z"}, + } + ], + "ctlogs": [], + "timestampAuthorities": [], + } + ) + ) + info = self._verify_synthetic( + signature, + str(root_path), + subject="https://example.com/id", + ignore_tlog=True, + ignore_tsa=True, + ignore_sct=True, + ) + assert info.oidc.subject == "https://example.com/id" + + def test_invalid_bundle(self) -> None: + with self.assertRaises(oidc.OIDCError): + oidc.verify_with_oidc( + CID.encode(), + sign_v1.VerifyWithOIDC(), + sign_v1.Signature(content_bundle="not json"), + ) + + def test_sign_requires_reachable_services(self) -> None: + with self.assertRaises(oidc.OIDCError): + oidc.sign_blob_with_oidc( + CID.encode(), sign_v1.SignWithOIDC(id_token="not-a-jwt") + ) + + +class TestKeysEdgeCases(unittest.TestCase): + @staticmethod + def _encrypted_pem(envelope: object | None = None, body: str | None = None) -> str: + if body is None: + body = base64.b64encode(json.dumps(envelope).encode()).decode() + return ( + "-----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY-----\n" + f"{body}\n" + "-----END ENCRYPTED SIGSTORE PRIVATE KEY-----\n" + ) + + def test_malformed_encrypted_keys(self) -> None: + good = { + "kdf": { + "name": "scrypt", + "params": {"N": 1024, "r": 8, "p": 1}, + "salt": base64.b64encode(b"salt").decode(), + }, + "cipher": { + "name": "nacl/secretbox", + "nonce": base64.b64encode(os.urandom(24)).decode(), + }, + "ciphertext": base64.b64encode(b"garbage").decode(), + } + bad_kdf = {**good, "kdf": {**good["kdf"], "name": "argon2"}} # type: ignore[dict-item] + bad_cipher = {**good, "cipher": {"name": "aes", "nonce": ""}} + missing = {k: v for k, v in good.items() if k != "ciphertext"} + cases = { + "unsupported KDF": self._encrypted_pem(bad_kdf), + "unsupported cipher": self._encrypted_pem(bad_cipher), + "invalid encrypted key format": self._encrypted_pem(missing), + "decrypt": self._encrypted_pem(good), + "invalid PEM body": self._encrypted_pem(body="abc"), + } + for expected, pem in cases.items(): + with self.subTest(expected), self.assertRaises(keys.KeyLoadError) as ctx: + keys.load_private_key(pem, b"pw") + assert expected in str(ctx.exception) + + def test_unsupported_key_types(self) -> None: + x25519_private = x25519.X25519PrivateKey.generate() + with self.assertRaises(keys.KeyLoadError): + keys.load_private_key(_pem_private(x25519_private)) + x25519_public = x25519_private.public_key().public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo + ) + with self.assertRaises(keys.KeyLoadError): + keys.load_public_key(x25519_public.decode()) + assert keys.detect_key_algorithm(x25519_public.decode()) == "unknown" + + def test_key_reference_errors(self) -> None: + with self.assertRaises(keys.KeyLoadError): + keys._read_key_ref("-----BEGIN nothing") + with self.assertRaises(keys.KeyLoadError): + keys.load_public_key("/definitely/not/a/key.pub") + with self.assertRaises(keys.KeyLoadError): + keys.load_public_key("-----BEGIN PUBLIC KEY-----\nbroken\n") + with tempfile.TemporaryDirectory() as tmp: + path = pathlib.Path(tmp) / "garbage.pub" + path.write_text("not a key") + with self.assertRaises(keys.KeyLoadError): + keys.load_public_key(str(path)) + + def test_detect_key_algorithm_fallbacks(self) -> None: + assert keys.detect_key_algorithm("not pem") == "unknown" + k1 = ec.generate_private_key(ec.SECP256K1()) + assert keys.detect_key_algorithm(keys.public_key_pem(k1)) == "ECDSA" + p384 = ec.generate_private_key(ec.SECP384R1()) + assert keys.detect_key_algorithm(keys.public_key_pem(p384)) == "ECDSA-P-384" + + def test_public_key_pem_accepts_public_keys(self) -> None: + key = ed25519.Ed25519PrivateKey.generate() + assert keys.public_key_pem(key.public_key()) == keys.public_key_pem(key) + + +class TestEd25519phEdgeCases(unittest.TestCase): + def test_invalid_inputs_are_rejected(self) -> None: + seed = os.urandom(32) + pub = ed25519ph.public_key(seed) + sig = ed25519ph.sign(seed, b"msg") + assert ed25519ph.verify(pub, b"msg", sig) + assert not ed25519ph.verify(pub, b"other", sig) + assert not ed25519ph.verify(pub[:31], b"msg", sig) + assert not ed25519ph.verify(pub, b"msg", sig[:63]) + # Non-canonical scalar (s >= L). + big_s = (2**252 + 27742317777372353535851937790883648493).to_bytes(32, "little") + assert not ed25519ph.verify(pub, b"msg", sig[:32] + big_s) + with self.assertRaises(ValueError): + ed25519ph.sign(b"short", b"msg") + with self.assertRaises(ValueError): + ed25519ph.public_key(b"short") + + +class ScriptedStore(FakeStore): + """FakeStore with injectable failures and extra referrer responses.""" + + def __init__(self) -> None: + super().__init__() + self.extra: list[store_v1.PullReferrerResponse] = [] + self.push_failures: list[int] = [] + self.pull_error: Exception | None = None + self.lookup_error: Exception | None = None + self._push_calls = 0 + + def push_referrer( + self, + req: list[store_v1.PushReferrerRequest], + metadata: Sequence[tuple[str, str]] | None = None, + ) -> list[store_v1.PushReferrerResponse]: + self._push_calls += 1 + if self._push_calls in self.push_failures: + msg = "push boom" + raise RuntimeError(msg) + return super().push_referrer(req, metadata) + + def pull_referrer( + self, + req: list[store_v1.PullReferrerRequest], + metadata: Sequence[tuple[str, str]] | None = None, + ) -> list[store_v1.PullReferrerResponse]: + if self.pull_error is not None: + raise self.pull_error + return super().pull_referrer(req, metadata) + self.extra + + def lookup( + self, + refs: list[core_v1.RecordRef], + metadata: Sequence[tuple[str, str]] | None = None, + ) -> list[core_v1.RecordMeta]: + if self.lookup_error is not None: + raise self.lookup_error + return super().lookup(refs, metadata) + + +class TestSignVerifyEdgeCases(unittest.TestCase): + def test_push_failures_are_reported(self) -> None: + for failing_call, expected in ((1, "public key"), (2, "signature")): + store = ScriptedStore() + store.push_failures = [failing_call] + with self.subTest(expected), self.assertRaises(RuntimeError) as ctx: + sign_record(store, _key_sign_request(COSIGN_KEY, b"pw")) + assert f"failed to store {expected}" in str(ctx.exception) + + def test_sign_rejects_unknown_provider(self) -> None: + provider = SimpleNamespace(WhichOneof=lambda _name: "kms") + req = SimpleNamespace( + provider=provider, + record_ref=core_v1.RecordRef(cid=CID), + HasField=lambda _name: True, + ) + with self.assertRaises(RuntimeError) as ctx: + sign_record(FakeStore(), req) # type: ignore[arg-type] + assert "unsupported signature provider type: kms" in str(ctx.exception) + + def test_sign_with_oidc_provider(self) -> None: + store = ScriptedStore() + req = sign_v1.SignRequest( + record_ref=core_v1.RecordRef(cid=CID), + provider=sign_v1.SignRequestProvider( + oidc=sign_v1.SignWithOIDC(id_token="token") + ), + ) + signature = sign_v1.Signature(signature="c2ln", content_bundle="{}") + with unittest.mock.patch.object( + oidc, + "sign_blob_with_oidc", + return_value=(signature, sign_v1.PublicKey(key="pem")), + ) as signer: + sign_record(store, req) + signer.assert_called_once() + assert len(store.pushed) == 2 + + with ( + unittest.mock.patch.object( + oidc, "sign_blob_with_oidc", side_effect=oidc.OIDCError("nope") + ), + self.assertRaises(RuntimeError) as ctx, + ): + sign_record(ScriptedStore(), req) + assert "failed to sign record" in str(ctx.exception) + + def test_unusable_referrers_are_skipped(self) -> None: + store = ScriptedStore() + sign_record(store, _key_sign_request(COSIGN_KEY, b"pw")) + store.extra = [ + store_v1.PullReferrerResponse(), # no referrer set + store_v1.PullReferrerResponse( + referrer=core_v1.RecordReferrer( + type="x", data={"signature": [1], "key": [1]} + ) + ), + ] + ref = core_v1.RecordRef(cid=CID) + assert len(verify_mod.pull_signatures(store, ref)) == 1 + assert verify_mod.pull_public_keys(store, ref) == [COSIGN_PUB] + response = verify_record(store, sign_v1.VerifyRequest(record_ref=ref)) + assert response.success + + def test_lookup_errors(self) -> None: + req = sign_v1.VerifyRequest(record_ref=core_v1.RecordRef(cid=CID)) + + class GrpcLikeError(Exception): + def details(self) -> str: + return "record not found in store" + + store = ScriptedStore() + store.lookup_error = GrpcLikeError() + response = verify_record(store, req) + assert response.error_message == "record not found" + + store.lookup_error = ConnectionError("down") + with self.assertRaises(RuntimeError) as ctx: + verify_record(store, req) + assert "failed to lookup record" in str(ctx.exception) + + def test_pull_failures(self) -> None: + store = ScriptedStore() + store.pull_error = ConnectionError("down") + req = sign_v1.VerifyRequest(record_ref=core_v1.RecordRef(cid=CID)) + with self.assertRaises(RuntimeError) as ctx: + verify_record(store, req) + assert "pull signatures" in str(ctx.exception) + + # Signatures succeed but the public key lookup fails. + signed = ScriptedStore() + sign_record(signed, _key_sign_request(COSIGN_KEY, b"pw")) + original = signed.pull_referrer + + def flaky( + req: list[store_v1.PullReferrerRequest], + metadata: Sequence[tuple[str, str]] | None = None, + ) -> list[store_v1.PullReferrerResponse]: + if req[0].referrer_type == referrer.PUBLIC_KEY_REFERRER_TYPE: + msg = "keys down" + raise ConnectionError(msg) + return original(req, metadata) + + signed.pull_referrer = flaky # type: ignore[method-assign] + with self.assertRaises(RuntimeError) as ctx: + verify_record(signed, req) + assert "pull public keys" in str(ctx.exception) + + def test_verify_with_fetcher_requires_cid(self) -> None: + with self.assertRaises(ValueError): + verify_mod.verify_with_fetcher(ScriptedStore(), sign_v1.VerifyRequest()) + + def test_verify_with_keys_edge_cases(self) -> None: + raw = base64.b64decode(COSIGN_BLOB_SIGNATURE) + payload = b"bafytestcid" + good = sign_v1.Signature(signature=COSIGN_BLOB_SIGNATURE) + # Unloadable keys are skipped; the next key still verifies. + info = verify_mod._verify_with_keys(payload, ["not a key", COSIGN_PUB], good) + assert info.key.public_key == COSIGN_PUB + # A signature that is not base64 is treated as raw bytes and rejected. + with self.assertRaises(ValueError): + verify_mod._verify_with_keys( + payload, [COSIGN_PUB], sign_v1.Signature(signature="@@not-base64@@") + ) + assert len(raw) > 0 + + def test_verify_with_any_errors(self) -> None: + options = sign_v1.VerifyOptionsOIDC() + no_key = sign_v1.Signature(signature=COSIGN_BLOB_SIGNATURE) + with self.assertRaises(ValueError) as ctx: + verify_mod._verify_with_any(b"other", [COSIGN_PUB], no_key, options) + assert "failed to verify signature with keys" in str(ctx.exception) + + bundled = sign_v1.Signature(signature="c2ln", content_bundle="not json") + with self.assertRaises(ValueError) as ctx: + verify_mod._verify_with_any(b"x", [], bundled, options) + assert "failed to verify signature with OIDC" in str(ctx.exception) + + def test_signer_key(self) -> None: + assert verify_mod._signer_key(sign_v1.SignerInfo()) == "" + key_signer = sign_v1.SignerInfo(key=sign_v1.SignerInfoKey(public_key="k")) + oidc_signer = sign_v1.SignerInfo(oidc=sign_v1.SignerInfoOIDC(issuer="i")) + assert verify_mod._signer_key(key_signer).startswith("key:") + assert verify_mod._signer_key(oidc_signer).startswith("oidc:") + + +class _Certs: + """Helpers building small throwaway certificates.""" + + @staticmethod + def build( + extensions: Sequence[tuple[x509.ExtensionType, bool]] = (), + key: object | None = None, + ) -> x509.Certificate: + key = key or ec.generate_private_key(ec.SECP256R1()) + now = datetime.now(timezone.utc) + builder = ( + x509.CertificateBuilder() + .subject_name(x509.Name([])) + .issuer_name( + x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "ca")]) + ) + .public_key(key.public_key()) # type: ignore[attr-defined] + .serial_number(1) + .not_valid_before(now) + .not_valid_after(now + timedelta(minutes=10)) + ) + for extension, critical in extensions: + builder = builder.add_extension(extension, critical) + algorithm = ( + None if isinstance(key, ed25519.Ed25519PrivateKey) else hashes.SHA256() + ) + return builder.sign(key, algorithm) # type: ignore[arg-type] + + +class TestOIDCEdgeCases(unittest.TestCase): + def test_der_utf8_string(self) -> None: + assert oidc._der_utf8_string(b"\x0c\x03abc") == "abc" + long_value = "a" * 200 + encoded = b"\x0c\x81" + bytes([200]) + long_value.encode() + assert oidc._der_utf8_string(encoded) == long_value + for bad in (b"", b"\x04\x01a"): + with self.assertRaises(oidc.OIDCError): + oidc._der_utf8_string(bad) + + def test_summarize_certificate_variants(self) -> None: + issuer_v1 = x509.UnrecognizedExtension(oidc._OID_ISSUER_V1, b"https://v1") + email = x509.SubjectAlternativeName([x509.RFC822Name("dev@example.com")]) + summary = oidc._summarize_certificate( + _Certs.build([(issuer_v1, False), (email, True)]) + ) + assert (summary.issuer, summary.subject) == ("https://v1", "dev@example.com") + + other = x509.SubjectAlternativeName( + [x509.OtherName(oidc._OID_OTHER_NAME_SAN, b"\x0c\x04user")] + ) + summary = oidc._summarize_certificate(_Certs.build([(other, True)])) + assert summary.subject == "user" + + unrelated = x509.SubjectAlternativeName([x509.DNSName("example.com")]) + for extensions in ([], [(unrelated, True)]): + with self.assertRaises(oidc.OIDCError): + oidc._summarize_certificate(_Certs.build(extensions)) + + def test_matches(self) -> None: + assert oidc._matches("", ".*", "anything") + assert oidc._matches("exact", "", "exact") + assert not oidc._matches("exact", "", "different") + assert not oidc._matches("", "^a", "b") + + def test_verify_signature_key_types(self) -> None: + digest = hashlib.sha256(b"payload").digest() + ec_key = ec.generate_private_key(ec.SECP256R1()) + signature = ec_key.sign(digest, ec.ECDSA(Prehashed(hashes.SHA256()))) + oidc._verify_signature(_Certs.build(key=ec_key), signature, digest) + with self.assertRaises(oidc.OIDCError): + oidc._verify_signature(_Certs.build(key=ec_key), signature, b"x" * 32) + + rsa_key = rsa.generate_private_key(65537, 2048) + rsa_signature = rsa_key.sign( + digest, padding.PKCS1v15(), Prehashed(hashes.SHA256()) + ) + rsa_cert = _Certs.build(key=rsa_key) + oidc._verify_signature(rsa_cert, rsa_signature, digest) + with self.assertRaises(oidc.OIDCError): + oidc._verify_signature(rsa_cert, b"\x00" * 256, digest) + + with self.assertRaises(oidc.OIDCError): + oidc._verify_signature( + _Certs.build(key=ed25519.Ed25519PrivateKey.generate()), b"", digest + ) + + def test_load_trusted_root_errors(self) -> None: + with self.assertRaises(oidc.OIDCError): + oidc._load_trusted_root( + sign_v1.VerifyOptionsOIDC(trusted_root_path="/no/such/root.json") + ) + with ( + unittest.mock.patch.object( + oidc, "TrustUpdater", side_effect=RuntimeError("offline") + ), + self.assertRaises(oidc.OIDCError) as ctx, + ): + oidc._load_trusted_root(sign_v1.VerifyOptionsOIDC()) + assert "TUF" in str(ctx.exception) + + def test_verify_signed_timestamp(self) -> None: + now = datetime.now(timezone.utc) + + def authority(count: int, start: datetime, end: datetime | None) -> object: + return SimpleNamespace( + certificates=lambda allow_expired: [object()] * count, # noqa: ARG005 + validity_period_start=start, + validity_period_end=end, + ) + + class FakeBuilder: + fail = False + + def tsa_certificate(self, _cert: object) -> FakeBuilder: + return self + + def add_root_certificate(self, _cert: object) -> FakeBuilder: + return self + + def add_intermediate_certificate(self, _cert: object) -> FakeBuilder: + return self + + def build(self) -> FakeBuilder: + return self + + def verify_message(self, _response: object, _message: bytes) -> None: + if FakeBuilder.fail: + msg = "bad timestamp" + raise oidc.Rfc3161VerificationError(msg) + + response = SimpleNamespace(tst_info=SimpleNamespace(gen_time=now)) + day = timedelta(days=1) + + def run(*authorities: object) -> object: + root = SimpleNamespace(get_timestamp_authorities=lambda: list(authorities)) + return oidc._verify_signed_timestamp(root, response, b"sig") # type: ignore[arg-type] + + with unittest.mock.patch.object(oidc, "VerifierBuilder", FakeBuilder): + FakeBuilder.fail = False + # Too few certificates, then a valid open-ended authority. + verified = run(authority(1, now - day, None), authority(3, now - day, None)) + assert verified is not None + assert verified.source == "TimestampAuthority" # type: ignore[attr-defined] + # Timestamp outside the authority's validity period. + assert run(authority(2, now - day, now - timedelta(hours=1))) is None + assert run(authority(2, now + day, None)) is None + # Verification failures move on to the next authority. + FakeBuilder.fail = True + assert run(authority(2, now - day, None)) is None + + def test_identity_token_interactive_flow(self) -> None: + issuer = unittest.mock.MagicMock() + with unittest.mock.patch.object(oidc, "Issuer", return_value=issuer) as cls: + result = oidc._identity_token( + "", + sign_v1.SignOptionsOIDC( + oidc_provider_url="https://idp.example", + oidc_client_id="client", + oidc_client_secret="secret", + ), + ) + cls.assert_called_once_with("https://idp.example") + issuer.identity_token.assert_called_once_with( + client_id="client", client_secret="secret" + ) + assert result is issuer.identity_token.return_value + + def test_sign_with_rekor_entry(self) -> None: + ca_key, ca_cert = _make_ca() + requests: list[dict[str, Any]] = [] + + class FakeFulcio: + def __init__(self, _url: str) -> None: + self.signing_cert = self + + def post(self, csr: x509.CertificateSigningRequest, _id: object) -> object: + public_key = csr.public_key() + assert isinstance(public_key, ec.EllipticCurvePublicKey) + return SimpleNamespace( + cert=_issue_leaf(ca_key, ca_cert, public_key, "https://x/id") + ) + + class FakeTSA: + def __init__(self, url: str) -> None: + self.url = url + + def request_timestamp(self, _signature: bytes) -> object: + return SimpleNamespace(as_bytes=lambda: b"ts") + + class FakeRekor: + def __init__(self, url: str) -> None: + requests.append({"url": url}) + + def _build_hashed_rekord_request(self, **kwargs: Any) -> str: # noqa: ANN401 + requests.append(kwargs) + return "request" + + def create_entry(self, request: str) -> str: + return f"entry:{request}" + + def from_parts(*parts: object) -> object: + requests.append({"parts": parts}) + return SimpleNamespace( + to_json=lambda: '{"rekor": true}', + _inner=SimpleNamespace(media_type="application/vnd.test+json"), + ) + + with ( + unittest.mock.patch.object(oidc, "FulcioClient", FakeFulcio), + unittest.mock.patch.object(oidc, "TimestampAuthorityClient", FakeTSA), + unittest.mock.patch.object(oidc, "RekorClient", FakeRekor), + unittest.mock.patch.object(oidc, "IdentityToken", lambda t, **_: t), + unittest.mock.patch.object(oidc.Bundle, "_from_parts", from_parts), + ): + signature, _ = oidc.sign_blob_with_oidc( + CID.encode(), + sign_v1.SignWithOIDC( + id_token="token", + options=sign_v1.SignOptionsOIDC(rekor_url="https://rekor.test"), + ), + ) + assert requests[0] == {"url": "https://rekor.test"} + assert signature.content_bundle == '{"rekor": true}' + assert signature.content_type == "application/vnd.test+json" + assert requests[-1]["parts"][2] == "entry:request" + + def test_verify_bundle_variants(self) -> None: + helper = TestOIDC("test_value_matcher") + relaxed: dict[str, Any] = { + "ignore_tlog": True, + "ignore_tsa": True, + "ignore_sct": True, + } + with tempfile.TemporaryDirectory() as tmp: + signature, root_path = helper._synthetic_signature(tmp) + bundle = json.loads(signature.content_bundle) + + def with_bundle(mutated: dict[str, Any]) -> sign_v1.Signature: + copy = sign_v1.Signature() + copy.CopyFrom(signature) + copy.content_bundle = json.dumps(mutated) + return copy + + # Bundle without a message signature. + no_message = {k: v for k, v in bundle.items() if k != "messageSignature"} + with self.assertRaises(oidc.OIDCError): + helper._verify_synthetic(with_bundle(no_message), root_path, **relaxed) + + # Bundle without any certificate. + no_cert = json.loads(signature.content_bundle) + no_cert["verificationMaterial"].pop("certificate") + with self.assertRaises(oidc.OIDCError): + helper._verify_synthetic(with_bundle(no_cert), root_path, **relaxed) + + # Pre-v0.3 style certificate chain. + chain = json.loads(signature.content_bundle) + leaf = chain["verificationMaterial"].pop("certificate") + chain["verificationMaterial"]["x509CertificateChain"] = { + "certificates": [leaf] + } + info = helper._verify_synthetic(with_bundle(chain), root_path, **relaxed) + assert info.oidc.issuer == "https://issuer.example" + + # A recorded digest that does not match the payload. + bad_digest = json.loads(signature.content_bundle) + bad_digest["messageSignature"]["messageDigest"]["digest"] = ( + base64.b64encode(b"\x00" * 32).decode() + ) + with self.assertRaises(oidc.OIDCError): + helper._verify_synthetic(with_bundle(bad_digest), root_path, **relaxed) + + # A required transparency log entry that is missing. + with self.assertRaises(oidc.OIDCError) as ctx: + helper._verify_synthetic( + signature, root_path, ignore_tsa=True, ignore_sct=True + ) + assert "log inclusion" in str(ctx.exception) + + # Without any usable time source nothing can be verified. + with self.assertRaises(oidc.OIDCError): + helper._verify_synthetic( + signature, + root_path, + ignore_tsa=True, + ignore_sct=True, + ignore_tlog=False, + ) + + def test_trusted_root_from_tuf(self) -> None: + updater = unittest.mock.MagicMock() + updater.get_trusted_root_path.return_value = "/tuf/trusted_root.json" + with ( + unittest.mock.patch.object( + oidc, "TrustUpdater", return_value=updater + ) as updater_cls, + unittest.mock.patch.object(oidc.TrustedRoot, "from_file") as from_file, + ): + root = oidc._load_trusted_root( + sign_v1.VerifyOptionsOIDC(tuf_mirror_url="https://tuf.example") + ) + updater_cls.assert_called_once_with("https://tuf.example") + from_file.assert_called_once_with("/tuf/trusted_root.json") + assert root is from_file.return_value + + def test_verify_with_log_entry_checks(self) -> None: + helper = TestOIDC("test_value_matcher") + with tempfile.TemporaryDirectory() as tmp: + signature, root_path = helper._synthetic_signature(tmp) + + def fake_bundle(kind: str, version: str) -> unittest.mock.MagicMock: + entry = SimpleNamespace( + _verify=lambda _keyring: None, + _inner=SimpleNamespace( + kind_version=SimpleNamespace(kind=kind, version=version) + ), + ) + return unittest.mock.MagicMock( + return_value=SimpleNamespace(log_entry=entry) + ) + + for kind, version, validator in ( + ("hashedrekord", "0.0.2", "_validate_hashedrekord_v002_entry_body"), + ("hashedrekord", "0.0.1", "_validate_hashedrekord_v001_entry_body"), + ): + with ( + self.subTest(version), + unittest.mock.patch.object( + oidc, "Bundle", fake_bundle(kind, version) + ), + unittest.mock.patch.object( + oidc.TrustedRoot, "rekor_keyring", return_value=None + ), + unittest.mock.patch.object(oidc, validator) as validate, + ): + # The log entry is accepted; without any time source the + # verification still fails closed. + with self.assertRaises(oidc.OIDCError) as ctx: + helper._verify_synthetic( + signature, root_path, ignore_tsa=True, ignore_sct=True + ) + validate.assert_called_once() + assert "no valid observer timestamps" in str(ctx.exception) + + with ( + unittest.mock.patch.object( + oidc, "Bundle", fake_bundle("intoto", "0.0.2") + ), + unittest.mock.patch.object( + oidc.TrustedRoot, "rekor_keyring", return_value=None + ), + self.assertRaises(oidc.OIDCError) as ctx, + ): + helper._verify_synthetic( + signature, root_path, ignore_tsa=True, ignore_sct=True + ) + assert "expected entry type hashedrekord" in str(ctx.exception) + + def test_verify_with_verified_timestamp(self) -> None: + helper = TestOIDC("test_value_matcher") + with tempfile.TemporaryDirectory() as tmp: + signature, root_path = helper._synthetic_signature(tmp) + bundle = json.loads(signature.content_bundle) + bundle["verificationMaterial"]["timestampVerificationData"] = { + "rfc3161Timestamps": [{"signedTimestamp": "AAAA"}] + } + signature.content_bundle = json.dumps(bundle) + verified = oidc._VerifiedTime( + "TimestampAuthority", datetime.now(timezone.utc) + ) + with ( + unittest.mock.patch.object( + oidc, + "TimestampVerificationData", + return_value=SimpleNamespace(rfc3161_timestamps=[object()]), + ), + unittest.mock.patch.object( + oidc, "_verify_signed_timestamp", return_value=verified + ), + ): + info = helper._verify_synthetic( + signature, root_path, ignore_tlog=True, ignore_sct=True + ) + assert info.oidc.issuer == "https://issuer.example" + + def test_verify_dsse_bundle_has_no_message_signature(self) -> None: + helper = TestOIDC("test_value_matcher") + with tempfile.TemporaryDirectory() as tmp: + signature, root_path = helper._synthetic_signature(tmp) + bundle = json.loads(signature.content_bundle) + bundle.pop("messageSignature") + bundle["dsseEnvelope"] = { + "payload": "e30=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [{"sig": "c2ln", "keyid": ""}], + } + signature.content_bundle = json.dumps(bundle) + with self.assertRaises(oidc.OIDCError) as ctx: + helper._verify_synthetic( + signature, + root_path, + ignore_tlog=True, + ignore_tsa=True, + ignore_sct=True, + ) + assert "no message signature" in str(ctx.exception) + + def test_verify_bundle_without_certificate(self) -> None: + helper = TestOIDC("test_value_matcher") + with tempfile.TemporaryDirectory() as tmp: + signature, root_path = helper._synthetic_signature(tmp) + bundle = json.loads(signature.content_bundle) + material = bundle["verificationMaterial"] + material.pop("certificate") + material["publicKey"] = {"hint": "key"} + signature.content_bundle = json.dumps(bundle) + with self.assertRaises(oidc.OIDCError) as ctx: + helper._verify_synthetic( + signature, + root_path, + ignore_tlog=True, + ignore_tsa=True, + ignore_sct=True, + ) + assert "signing certificate" in str(ctx.exception) + + def test_verify_timestamps_and_sct_failures(self) -> None: + helper = TestOIDC("test_value_matcher") + with tempfile.TemporaryDirectory() as tmp: + signature, root_path = helper._synthetic_signature(tmp) + # With the log ignored, an SCT is still required and absent. + with self.assertRaises(oidc.OIDCError) as ctx: + helper._verify_synthetic( + signature, root_path, ignore_tlog=True, ignore_tsa=True + ) + assert "signed certificate timestamp" in str(ctx.exception) + # A timestamp is required and absent. + with self.assertRaises(oidc.OIDCError) as ctx: + helper._verify_synthetic( + signature, root_path, ignore_tlog=True, ignore_sct=True + ) + assert "timestamps" in str(ctx.exception) + + +if __name__ == "__main__": + unittest.main() diff --git a/dir-sdk-python/agntcy/dir_sdk/tests/test_transport.py b/dir-sdk-python/agntcy/dir_sdk/tests/test_transport.py new file mode 100644 index 0000000..ffb7224 --- /dev/null +++ b/dir-sdk-python/agntcy/dir_sdk/tests/test_transport.py @@ -0,0 +1,108 @@ +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the SPIFFE transport helpers that do not need a SPIRE agent.""" + +from __future__ import annotations + +import sys +import unittest +import unittest.mock +from datetime import datetime, timedelta, timezone +from types import SimpleNamespace +from typing import Any + +from agntcy.dir_sdk.client.config import Config +from agntcy.dir_sdk.client.transport import channels +from agntcy.dir_sdk.client.transport.interceptors import JWTAuthInterceptor +from cryptography import x509 +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec + + +class TestSpiffeTransport(unittest.TestCase): + def test_jwt_interceptor_creates_workload_client_lazily(self) -> None: + workload_client = unittest.mock.MagicMock() + fake_spiffe = SimpleNamespace(WorkloadApiClient=workload_client) + with unittest.mock.patch.dict(sys.modules, {"spiffe": fake_spiffe}): + interceptor = JWTAuthInterceptor("unix:///spire.sock", "audience") + workload_client.assert_called_once_with(socket_path="unix:///spire.sock") + assert interceptor._workload_client is workload_client.return_value + + def test_channels_require_spiffe_socket_path(self) -> None: + config = Config(spiffe_socket_path="") + with self.assertRaises(ValueError): + channels.create_x509_channel(config) + with self.assertRaises(ValueError): + channels.create_jwt_channel(config) + + def test_jwt_channel_requires_audience(self) -> None: + config = Config(spiffe_socket_path="unix:///spire.sock", jwt_audience="") + with self.assertRaises(ValueError): + channels.create_jwt_channel(config) + + +def _self_signed_certificate() -> tuple[x509.Certificate, ec.EllipticCurvePrivateKey]: + key = ec.generate_private_key(ec.SECP256R1()) + name = x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "spire")]) + now = datetime.now(timezone.utc) + cert = ( + x509.CertificateBuilder() + .subject_name(name) + .issuer_name(name) + .public_key(key.public_key()) + .serial_number(1) + .not_valid_before(now) + .not_valid_after(now + timedelta(hours=1)) + .sign(key, hashes.SHA256()) + ) + return cert, key + + +class TestSpiffeChannels(unittest.TestCase): + def _fake_spiffe(self, *, with_bundle: bool = True) -> tuple[SimpleNamespace, Any]: + cert, key = _self_signed_certificate() + source = unittest.mock.MagicMock() + source.bundles = ( + [SimpleNamespace(x509_authorities=[cert])] if with_bundle else [] + ) + source.svid = SimpleNamespace(private_key=key, leaf=cert) + fake = SimpleNamespace( + WorkloadApiClient=unittest.mock.MagicMock(), + X509Source=unittest.mock.MagicMock(return_value=source), + ) + return fake, source + + def test_x509_channel(self) -> None: + fake, _ = self._fake_spiffe() + config = Config(spiffe_socket_path="unix:///spire.sock") + with unittest.mock.patch.dict(sys.modules, {"spiffe": fake}): + channel = channels.create_x509_channel(config) + channel.close() + fake.WorkloadApiClient.assert_called_once_with(socket_path="unix:///spire.sock") + + def test_jwt_channel(self) -> None: + fake, source = self._fake_spiffe() + config = Config( + spiffe_socket_path="unix:///spire.sock", jwt_audience="directory" + ) + with unittest.mock.patch.dict(sys.modules, {"spiffe": fake}): + channel = channels.create_jwt_channel(config) + channel.close() + source.close.assert_called_once() + + def test_jwt_channel_requires_bundle(self) -> None: + fake, source = self._fake_spiffe(with_bundle=False) + config = Config( + spiffe_socket_path="unix:///spire.sock", jwt_audience="directory" + ) + with ( + unittest.mock.patch.dict(sys.modules, {"spiffe": fake}), + self.assertRaises(RuntimeError), + ): + channels.create_jwt_channel(config) + source.close.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/dir-sdk-python.md b/docs/dir-sdk-python.md index a757cec..c4e7aca 100644 --- a/docs/dir-sdk-python.md +++ b/docs/dir-sdk-python.md @@ -18,7 +18,7 @@ Source and issue tracker: [github.com/agntcy/dir-sdk-python](https://github.com/ | **Naming** | `resolve`, `get_verification_info` | | **Sync** | `create_sync`, `get_sync`, `list_syncs`, `delete_sync` | | **Events** | `listen` (server-streaming) | -| **Signing** | `sign` (local via `dirctl`), `verify` (remote gRPC) | +| **Signing** | `sign` (native, cosign/Sigstore compatible), `verify` (local or `from_server` gRPC) | ## Installation @@ -43,7 +43,6 @@ Requires [uv](https://github.com/astral-sh/uv). ```bash # Insecure (local development) export DIRECTORY_CLIENT_SERVER_ADDRESS="localhost:8888" -export DIRCTL_PATH="/path/to/dirctl" # X.509 / SPIFFE export DIRECTORY_CLIENT_SERVER_ADDRESS="localhost:8888" @@ -78,7 +77,6 @@ from agntcy.dir_sdk.client import Config, Client # Insecure (local development only) config = Config( server_address="localhost:8888", - dirctl_path="/usr/local/bin/dirctl", ) client = Client(config) @@ -100,30 +98,6 @@ config = Config( client = Client(config) ``` -### Docker-based dirctl - -If you don't have `dirctl` installed locally you can run it via Docker by passing a -`DockerConfig` instead of `dirctl_path`: - -```python -from agntcy.dir_sdk.client import Config, Client -from agntcy.dir_sdk.client.config import DockerConfig - -config = Config( - server_address="localhost:8888", - docker_config=DockerConfig( - dirctl_image="ghcr.io/agntcy/dir-ctl", - dirctl_image_tag="latest", - ), -) -client = Client(config) -``` - -!!! note - - `dirctl_path` and `docker_config` are mutually exclusive. Setting both raises a - `ValueError`. - ## OAuth 2.0 / OIDC bearer auth Use `auth_mode="oidc"` when your deployment expects a Bearer token on gRPC (e.g. via an @@ -235,14 +209,10 @@ client.unpublish(routing_v1.UnpublishRequest(cid=cid)) from agntcy.dir_sdk.models import search_v1 # Full-text / semantic record search -results = client.search_records( - search_v1.SearchRecordsRequest(query="my-agent") -) +results = client.search_records(search_v1.SearchRecordsRequest(query="my-agent")) # CID-only search -cids = client.search_cids( - search_v1.SearchCIDsRequest(query="my-agent") -) +cids = client.search_cids(search_v1.SearchCIDsRequest(query="my-agent")) ``` ### Naming — resolve and verify @@ -269,14 +239,36 @@ for event in stream: ### Signing and verification ```python -from agntcy.dir_sdk.models import sign_v1 +from agntcy.dir_sdk.models import core_v1, sign_v1 + +ref = core_v1.RecordRef(cid=cid) + +# Sign a record with a cosign-compatible private key (PEM content or key file path). +# The signature and public key are stored as referrers of the record. +client.sign( + sign_v1.SignRequest( + record_ref=ref, + provider=sign_v1.SignRequestProvider( + key=sign_v1.SignWithKey(private_key="cosign.key", password=b"secret") + ), + ) +) + +# Or sign keylessly with an OIDC identity token (Sigstore Fulcio/Rekor/TSA). +client.sign( + sign_v1.SignRequest( + record_ref=ref, + provider=sign_v1.SignRequestProvider(oidc=sign_v1.SignWithOIDC(id_token=token)), + ) +) -# Sign a record locally (requires dirctl binary) -client.sign(sign_v1.SignRequest(cid=cid)) +# Verify locally against the signatures stored with the record. +# Without a provider, any signer is accepted (keys and OIDC bundles). +resp = client.verify(sign_v1.VerifyRequest(record_ref=ref)) +print(resp.success, [s.WhichOneof("type") for s in resp.signers]) -# Verify a record signature via gRPC -resp = client.verify(sign_v1.VerifyRequest(cid=cid)) -print(resp.valid) +# Or fetch the server's cached verification result. +resp = client.verify(sign_v1.VerifyRequest(record_ref=ref, from_server=True)) ``` ## Error handling @@ -310,7 +302,6 @@ Common status codes: `NOT_FOUND`, `ALREADY_EXISTS`, `UNAVAILABLE`, `PERMISSION_D - Python 3.10 or higher - [uv](https://github.com/astral-sh/uv) package manager -- [dirctl](https://github.com/agntcy/dir/releases) CLI binary (signing operations only) - A running Directory server — see [Quickstart](dir-quickstart.md) or [Kubernetes Deployment](dir-deployment-kubernetes.md) diff --git a/examples/uv.lock b/examples/uv.lock index 76e10d9..bb0787f 100644 --- a/examples/uv.lock +++ b/examples/uv.lock @@ -1,6 +1,10 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] [[package]] name = "agntcy-dir" @@ -10,10 +14,13 @@ dependencies = [ { name = "agntcy-dir-grpc-python" }, { name = "agntcy-dir-protocolbuffers-python" }, { name = "authlib" }, + { name = "cryptography" }, { name = "grpcio" }, { name = "httpx" }, { name = "protobuf" }, { name = "pyasn1" }, + { name = "pynacl" }, + { name = "sigstore" }, { name = "spiffe" }, { name = "spiffe-tls" }, ] @@ -23,10 +30,13 @@ requires-dist = [ { name = "agntcy-dir-grpc-python", specifier = "==1.83.1.3.20261015113725+c24aeb0a65d8", index = "https://buf.build/gen/python" }, { name = "agntcy-dir-protocolbuffers-python", specifier = "==36.1.0.1.20261015113725+c24aeb0a65d8", index = "https://buf.build/gen/python" }, { name = "authlib", specifier = ">=1.6.9" }, + { name = "cryptography", specifier = ">=46.0.0" }, { name = "grpcio", specifier = ">=1.78.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "protobuf", specifier = ">=7.36.1" }, { name = "pyasn1", specifier = ">=0.6.3" }, + { name = "pynacl", specifier = ">=1.6.0" }, + { name = "sigstore", specifier = ">=4.5.0,<5" }, { name = "spiffe", specifier = ">=0.2.6" }, { name = "spiffe-tls", specifier = ">=0.3.1" }, ] @@ -83,6 +93,15 @@ wheels = [ { url = "https://buf.build/gen/python/agntcy-dir-protocolbuffers-python/agntcy_dir_protocolbuffers_python-36.1.0.1.20261015113725+c24aeb0a65d8-py3-none-any.whl", upload-time = "2026-10-15T11:37:25Z" }, ] +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + [[package]] name = "anyio" version = "4.13.0" @@ -237,6 +256,169 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/77/46e87bcfc45d25ab4db7cfc9bb544bfa3ffd302289ed31ae93f5433eb899/charset_normalizer-3.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3", size = 373348, upload-time = "2026-09-30T04:34:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/52/fc/e518013affcc43c9f919c3ba41bffe9b4ee4aceb0a6462a6243efcca5f2d/charset_normalizer-3.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e", size = 260860, upload-time = "2026-09-30T04:34:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ad/2a895c945ee61988dfd9ccee64f0b78dc09f29c9b34d1dd545246d78e0ad/charset_normalizer-3.5.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5", size = 246491, upload-time = "2026-09-30T04:34:46.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/05/5d958bc8ea503e26be25ada5430fd409cfb45dc22ff33f9f96e649613c99/charset_normalizer-3.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa", size = 290875, upload-time = "2026-09-30T04:34:47.691Z" }, + { url = "https://files.pythonhosted.org/packages/55/e2/06bad57dfdb49cad92c0ba85b6b4fa5827a67df37897287cfef0553843c2/charset_normalizer-3.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320", size = 286987, upload-time = "2026-09-30T04:34:49.356Z" }, + { url = "https://files.pythonhosted.org/packages/c4/9f/9f52d2886d52645987d603425482c63c5045a3005db1354a7097e5ed1ae9/charset_normalizer-3.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249", size = 271105, upload-time = "2026-09-30T04:34:50.957Z" }, + { url = "https://files.pythonhosted.org/packages/4a/41/e05e19578b7b87e7db2c3ecb884bd09d065274ab41e1f535542e8bae9b06/charset_normalizer-3.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e", size = 269833, upload-time = "2026-09-30T04:34:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cd/fc7152414561ff78f9c61a4a627025f65fd244abaefc99ac4e75c6169b33/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9", size = 263048, upload-time = "2026-09-30T04:34:54.013Z" }, + { url = "https://files.pythonhosted.org/packages/71/71/fb379e399b1013962716a059d551e03bd80b3e02f05d81246886af6c0958/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5", size = 249237, upload-time = "2026-09-30T04:34:55.373Z" }, + { url = "https://files.pythonhosted.org/packages/33/4f/aeadccd6d20882909eb2597ec40eccc05d505e2262f14dc76d7620700657/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c", size = 290756, upload-time = "2026-09-30T04:34:56.867Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7b/e8a92613236b257d3b75d532496eb21f68ad8231d7d56df94497e460112e/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c", size = 269026, upload-time = "2026-09-30T04:34:58.278Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ac/21d5c6b972285c5f095ff78afdc99f3539e3985dfcf7ff1cfbe9e772f529/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be", size = 287601, upload-time = "2026-09-30T04:34:59.703Z" }, + { url = "https://files.pythonhosted.org/packages/59/72/263491ec1494a194b16fcbff88a0220f2af633738c79e92b4d7189226322/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45", size = 272660, upload-time = "2026-09-30T04:35:01.26Z" }, + { url = "https://files.pythonhosted.org/packages/38/7a/81f1186c57e406233969ae96b5fd0c907254ab8b6e47da8ae24cd20fb9d7/charset_normalizer-3.5.2-cp310-cp310-win32.whl", hash = "sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4", size = 187301, upload-time = "2026-09-30T04:35:02.659Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/fb8b5178fdf7bf68efa61cbe892286bc9f73846f26147d4725fb89388475/charset_normalizer-3.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275", size = 215446, upload-time = "2026-09-30T04:35:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/36/40/e270b74e0d3f583d78d902960845a149e80eb7c737ee5613ccc6efc7264b/charset_normalizer-3.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c", size = 203389, upload-time = "2026-09-30T04:35:05.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/67/6a0b94a7960d5e1b5eacd2fb529f3fccc47db4644f7f0a7cfdcfc3be578a/charset_normalizer-3.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6", size = 370642, upload-time = "2026-09-30T04:35:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/01009e13b94041599004edf32e56e382c24e570f60f79bab8efe45cfe1eb/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5", size = 259280, upload-time = "2026-09-30T04:35:08.448Z" }, + { url = "https://files.pythonhosted.org/packages/66/85/3b5358f60a13210f0b67d3755c168ef758701b021e655d88d4da28554467/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74", size = 245335, upload-time = "2026-09-30T04:35:10.104Z" }, + { url = "https://files.pythonhosted.org/packages/74/75/77c1c479b09ecd751d1e767b251ea5c14d4d50ff757bf404afab2692f600/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab", size = 291707, upload-time = "2026-09-30T04:35:11.575Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0d/363f78cacb70f58f15f4b083961bbd9d292f335d3f5c66fc4f1cfe69cb90/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c", size = 286747, upload-time = "2026-09-30T04:35:13.022Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ed/cf505d3011ffceb12c2067a7a5d3cfe92b875d4d44bb0ff0d69375e2c184/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f", size = 269972, upload-time = "2026-09-30T04:35:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/15/d8/f0a93a431d170e7ca681d4f6650fee3de934d18560e474e7267eb4b0f987/charset_normalizer-3.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288", size = 269339, upload-time = "2026-09-30T04:35:16.087Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/9b2bd1c5b7af02462c9752d33994834ff972a96b4c483eefde9e594488e2/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400", size = 261683, upload-time = "2026-09-30T04:35:17.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/a5/cac540ab0fd61f3fec88ad3dbb64509e71424593d73cfdfff5ab3e4db279/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd", size = 248143, upload-time = "2026-09-30T04:35:18.849Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/ff467301deef2089fad87f72df9e000a26a78fec7acbb18e1999371b8369/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37", size = 292358, upload-time = "2026-09-30T04:35:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/22d7e785d1e210afc2e2f58600dd1799d17a35665faf84383f002826c5f8/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac", size = 268766, upload-time = "2026-09-30T04:35:21.72Z" }, + { url = "https://files.pythonhosted.org/packages/ae/91/e8e946267f1c2d9e2bd651726e2fbd2addf02c4d36cea5069e32ca9d7bb5/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a", size = 287432, upload-time = "2026-09-30T04:35:23.273Z" }, + { url = "https://files.pythonhosted.org/packages/4e/88/7561d8a88d555e7df6623abe7c0070b4baf47549b9408783a2ae0a1a6cf7/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640", size = 272470, upload-time = "2026-09-30T04:35:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/35/7e/578c702301ec036f01455f30744a08d2b42f6ab35b9b2d4bf8cae0ef2a80/charset_normalizer-3.5.2-cp311-cp311-win32.whl", hash = "sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d", size = 186688, upload-time = "2026-09-30T04:35:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/e8/fc/fdf8cf52ff21cd5bf158f20978991cf985325842f74283eb6df26c8a39d8/charset_normalizer-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96", size = 214932, upload-time = "2026-09-30T04:35:27.796Z" }, + { url = "https://files.pythonhosted.org/packages/97/66/3e45a506d8110b632541faf9a9470185aa9878f1ed44020f31346c1c5e5b/charset_normalizer-3.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1", size = 202828, upload-time = "2026-09-30T04:35:29.259Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/693809898870237d82785a03f3b2b58fe4c9f14669f84a7d4e623c92a59e/charset_normalizer-3.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491", size = 367780, upload-time = "2026-09-30T04:35:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/c9/87/2fea8c13dc24b3ca9c6f803a5b2dfdeae73eb4f9e12c7885ed908ff0433c/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c", size = 246730, upload-time = "2026-09-30T04:35:32.286Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9e/09efac30b937722f46d3110ba30b875b24b2e3a266ed746cc4e376a94d80/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0", size = 237707, upload-time = "2026-09-30T04:35:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/9e/18/70d76670b13686237863a379928d60bd10e021f17d243ab3d7014c4a5f4e/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51", size = 273050, upload-time = "2026-09-30T04:35:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/54/e2/77a8b09d5adc013ed07b95b01b8b8fa5441c4e810e83ee7e4aae2fa4d91a/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5", size = 270345, upload-time = "2026-09-30T04:35:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c5/38806a25ab5e65fc178f39affeda20858efafede2fce1ffc2556cfc9fe73/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649", size = 257601, upload-time = "2026-09-30T04:35:37.919Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/213565184708fdb263ae55e2c04ee1ff748129dd65d48ed0e3502da9c85a/charset_normalizer-3.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e", size = 252222, upload-time = "2026-09-30T04:35:39.544Z" }, + { url = "https://files.pythonhosted.org/packages/7e/24/76d2cefc25472531e4c5c7dfff68865eb1c39b78482f0fdc15b46f047830/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346", size = 248482, upload-time = "2026-09-30T04:35:41.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/dc/65a801b66ab4c197e22c433ab25e7ac24324ac6f45a2269aca42cce309bf/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1", size = 241206, upload-time = "2026-09-30T04:35:42.59Z" }, + { url = "https://files.pythonhosted.org/packages/a7/95/ca9b5eabde673002c6f1e7ada1b223916fe18f6d661da7aabd4d643718f1/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875", size = 273190, upload-time = "2026-09-30T04:35:44.347Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/803b4d2a3f6e1740f63f1e87b04d14b42f3d4fdfe6ed7d4db2d34102b14f/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1", size = 253527, upload-time = "2026-09-30T04:35:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/55/93c0e5dbd085ae0471346026abbe7e0db9ea2d6fea74e51f0b5a46f233a7/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413", size = 271285, upload-time = "2026-09-30T04:35:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/95/69/0dbd0e0b9b16cfa816cdfcb3e2e3854a1f680dc07fb1245ea125e7448060/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869", size = 260010, upload-time = "2026-09-30T04:35:48.996Z" }, + { url = "https://files.pythonhosted.org/packages/58/9d/e7b88e7b1bf403590c3b573277b5e1e488c68c7a6fbacca310a2c324e90c/charset_normalizer-3.5.2-cp312-cp312-win32.whl", hash = "sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e", size = 184125, upload-time = "2026-09-30T04:35:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/e6e083884cbcfd49c64865af05027fe7011be7b2d9179524f099a1b611f3/charset_normalizer-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc", size = 207486, upload-time = "2026-09-30T04:35:52.194Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e3/017aea0911ada7405a825c7d937eb3a13009664e2f5b38e8c4bbf2abf894/charset_normalizer-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3", size = 196734, upload-time = "2026-09-30T04:35:53.636Z" }, + { url = "https://files.pythonhosted.org/packages/c5/34/68292d68512768591aaff07c59bb53ee31341c87759433a859c4641a50c2/charset_normalizer-3.5.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5", size = 225946, upload-time = "2026-09-30T04:35:55.313Z" }, + { url = "https://files.pythonhosted.org/packages/e3/80/bee0b01b90ccd5322ae1d0abb33fab1bd95b7c2eadaf02aeccf22e04ee83/charset_normalizer-3.5.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e", size = 238619, upload-time = "2026-09-30T04:35:56.863Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/60ce52a85a7fd631ae8482ae6d74521014ca2f255892679484dc04d7ef56/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a", size = 206701, upload-time = "2026-09-30T04:35:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/36/8c/71aafad23f971afc84c2b295bc0c560739ce1dac558aad9fec22e39f3639/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d", size = 210506, upload-time = "2026-09-30T04:36:00.147Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/3c5a7798c046df7d2d68ad653cf5b6c5a8bfee225055a843c6f2f42aac1a/charset_normalizer-3.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055", size = 366626, upload-time = "2026-09-30T04:36:01.77Z" }, + { url = "https://files.pythonhosted.org/packages/e1/16/710ac3de2ee354e2bd1a9c94efe45a2d27b5c6ad39b2d6a905be2c094b6c/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858", size = 244624, upload-time = "2026-09-30T04:36:03.389Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/45c7439f5b63d24f7d5b2a1d760f34af7628782d7144b4cc8ded45c2d4bc/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234", size = 235731, upload-time = "2026-09-30T04:36:04.987Z" }, + { url = "https://files.pythonhosted.org/packages/4d/34/38f3154785ce92e9f56eb226f4d35bdfae6b008480dd055f58837a89c810/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21", size = 270352, upload-time = "2026-09-30T04:36:06.412Z" }, + { url = "https://files.pythonhosted.org/packages/04/f3/859f74e7babc977705026b30593b3be04049632a522fb7000f83c033d747/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718", size = 267393, upload-time = "2026-09-30T04:36:07.865Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/41d27f234b82e47c167a5f6c0f62501dc0c640585ff4aba79e08a390336a/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4", size = 254660, upload-time = "2026-09-30T04:36:09.248Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/5d1a997587febe5b26d8daffe363b5c1a091cece19828eec6502fd09c5ef/charset_normalizer-3.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3", size = 249996, upload-time = "2026-09-30T04:36:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1f/d1e78246f7ed60c8c8d606b4ac27f66ce49cc3e95f24893ccbeba9f77302/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c", size = 247028, upload-time = "2026-09-30T04:36:12.294Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/eba316edd4f0c4d3a5d945924c4eeeae59abac4056aa815d8a4268f863a2/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429", size = 240562, upload-time = "2026-09-30T04:36:13.887Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8e/aaa037d40ca9ef045977f1a661048b1aa33f223adfce3452fe9be9f79d14/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f", size = 271699, upload-time = "2026-09-30T04:36:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/26/19/1c1c9f75974adf523b87f34b8a2adc5a435cd65916812bcbd0dfa45f9a29/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a", size = 252030, upload-time = "2026-09-30T04:36:16.839Z" }, + { url = "https://files.pythonhosted.org/packages/bc/90/0660ef18e18df0a4d2a1a0edff7dfbba42d4e50ef2425557a5bb7051f77b/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00", size = 268565, upload-time = "2026-09-30T04:36:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/79/ba/57adc269824e8658f1a0f97a9e514c247445a9632b3419b97e0ba37f16dc/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d", size = 257627, upload-time = "2026-09-30T04:36:19.938Z" }, + { url = "https://files.pythonhosted.org/packages/9a/85/33abd4315c052d3d4f54c92b1ee49bfbc0dc7115a981e462a793b6d2ab87/charset_normalizer-3.5.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3", size = 143407, upload-time = "2026-09-30T04:36:21.376Z" }, + { url = "https://files.pythonhosted.org/packages/4f/de/6435e18d1aaa5d910b896d551411c96af1f42a0c56c29afc2016c61ccc2e/charset_normalizer-3.5.2-cp313-cp313-win32.whl", hash = "sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd", size = 183207, upload-time = "2026-09-30T04:36:22.776Z" }, + { url = "https://files.pythonhosted.org/packages/9c/76/b8ec57f4e9ee3253541abf95e4a462c0175fe8032dcd070f1f2421240942/charset_normalizer-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639", size = 206489, upload-time = "2026-09-30T04:36:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/c647c6ae47480221e875ea5d743ff94946f7416e3c69415ab772928e8d32/charset_normalizer-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3", size = 195501, upload-time = "2026-09-30T04:36:25.846Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" }, + { url = "https://files.pythonhosted.org/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" }, + { url = "https://files.pythonhosted.org/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" }, + { url = "https://files.pythonhosted.org/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" }, + { url = "https://files.pythonhosted.org/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" }, + { url = "https://files.pythonhosted.org/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" }, + { url = "https://files.pythonhosted.org/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" }, + { url = "https://files.pythonhosted.org/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" }, + { url = "https://files.pythonhosted.org/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" }, + { url = "https://files.pythonhosted.org/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" }, + { url = "https://files.pythonhosted.org/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" }, + { url = "https://files.pythonhosted.org/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" }, + { url = "https://files.pythonhosted.org/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" }, + { url = "https://files.pythonhosted.org/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", size = 367676, upload-time = "2026-09-30T04:37:28.329Z" }, + { url = "https://files.pythonhosted.org/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", size = 248414, upload-time = "2026-09-30T04:37:30.167Z" }, + { url = "https://files.pythonhosted.org/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", size = 244775, upload-time = "2026-09-30T04:37:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", size = 273877, upload-time = "2026-09-30T04:37:33.661Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", size = 269455, upload-time = "2026-09-30T04:37:35.573Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", size = 257336, upload-time = "2026-09-30T04:37:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", size = 257584, upload-time = "2026-09-30T04:37:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", size = 251854, upload-time = "2026-09-30T04:37:41.23Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", size = 248767, upload-time = "2026-09-30T04:37:42.883Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", size = 274479, upload-time = "2026-09-30T04:37:44.658Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", size = 258310, upload-time = "2026-09-30T04:37:46.529Z" }, + { url = "https://files.pythonhosted.org/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", size = 271100, upload-time = "2026-09-30T04:37:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", size = 259936, upload-time = "2026-09-30T04:37:50.023Z" }, + { url = "https://files.pythonhosted.org/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", size = 186301, upload-time = "2026-09-30T04:37:51.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", size = 210858, upload-time = "2026-09-30T04:37:53.427Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", size = 200309, upload-time = "2026-09-30T04:37:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", size = 395343, upload-time = "2026-09-30T04:37:56.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", size = 250114, upload-time = "2026-09-30T04:37:58.607Z" }, + { url = "https://files.pythonhosted.org/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", size = 239997, upload-time = "2026-09-30T04:38:00.511Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", size = 272313, upload-time = "2026-09-30T04:38:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", size = 269986, upload-time = "2026-09-30T04:38:04.435Z" }, + { url = "https://files.pythonhosted.org/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", size = 260202, upload-time = "2026-09-30T04:38:06.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", size = 259083, upload-time = "2026-09-30T04:38:08.311Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", size = 253081, upload-time = "2026-09-30T04:38:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", size = 248712, upload-time = "2026-09-30T04:38:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", size = 274176, upload-time = "2026-09-30T04:38:14.138Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", size = 259413, upload-time = "2026-09-30T04:38:15.841Z" }, + { url = "https://files.pythonhosted.org/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", size = 269434, upload-time = "2026-09-30T04:38:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", size = 262716, upload-time = "2026-09-30T04:38:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", size = 197159, upload-time = "2026-09-30T04:38:21.037Z" }, + { url = "https://files.pythonhosted.org/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", size = 221322, upload-time = "2026-09-30T04:38:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", size = 209192, upload-time = "2026-09-30T04:38:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" }, + { url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" }, +] + [[package]] name = "cryptography" version = "48.0.0" @@ -312,12 +494,50 @@ requires-dist = [ { name = "httpx", specifier = ">=0.28.1" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "dnspython" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4a/50822184bd67cc6493f0fb6a880749158fcd31ab3fa07409acfd91f9fc85/dnspython-2.9.0.tar.gz", hash = "sha256:b44dc6b18f07a8b1c56676a19fbfdb5209415b046a9cece286baafa87ff3f7f1", size = 423560, upload-time = "2026-10-09T00:07:24.352Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/02/cdcc9b7c051786a103c3b09e1003a82fa0c66bcb91ffbdabcfbf7b4163b9/dnspython-2.9.0-py3-none-any.whl", hash = "sha256:9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9", size = 354822, upload-time = "2026-10-09T00:07:22.622Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "dnspython", version = "2.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -458,6 +678,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "id" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, +] + [[package]] name = "idna" version = "3.18" @@ -467,6 +699,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] +[[package]] +name = "importlib-resources" +version = "5.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/f1/8711c49ffd121083007a24c1bff0d324c9ff621d4fdf8b4ffcb8d9e60330/importlib_resources-5.13.0.tar.gz", hash = "sha256:82d5c6cca930697dbbd86c93333bb2c2e72861d4789a11c2662b933e5ad2b528", size = 36550, upload-time = "2023-07-07T16:21:00.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/68/bd9dd6bbf06772c7accce77d0354d783333fbe712a60b08fc13540c05422/importlib_resources-5.13.0-py3-none-any.whl", hash = "sha256:9f7bd0c97b79972a6cce36a366356d16d5e13b09679c11a58f1014bfdf8e64b2", size = 32912, upload-time = "2023-07-07T16:20:58.128Z" }, +] + [[package]] name = "joserfc" version = "1.7.0" @@ -479,6 +720,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/83/b6b62a66a06ce872d9429a5eb5ee20b2002fd9c331b953c94381c1f7c9f9/joserfc-1.7.0-py3-none-any.whl", hash = "sha256:17e5d7a5a35e65442b05efc435a3d5d46696ffa2c8a2ed0eea6f63fc268e3224", size = 70387, upload-time = "2026-06-02T09:59:33.264Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "pem" version = "23.1.0" @@ -488,6 +750,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/97/8299a481ae6c08494b5d53511e6a4746775d8a354c685c69d8796b2ed482/pem-23.1.0-py3-none-any.whl", hash = "sha256:78bbb1e75b737891350cb9499cbba31da5d59545f360f44163c0bc751cad55d3", size = 9195, upload-time = "2023-06-21T10:24:39.164Z" }, ] +[[package]] +name = "platformdirs" +version = "4.12.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/90/a1/d5f9002a70298c64a789779077d8dd90c10aa1f47fe40c86802df874f2a6/platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250", size = 60010, upload-time = "2026-10-07T23:30:32.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/ba/223e00b885e960edd5d4b4d178c88acce019bd5b83786093681b8c393492/platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7", size = 32596, upload-time = "2026-10-07T23:30:30.825Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", size = 62362, upload-time = "2026-10-19T01:02:11.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/8a/1d8cb3842909dc193a73835adedf412e32e18f8ffd59f43983949622fe6f/platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e", size = 32779, upload-time = "2026-10-19T01:02:10.17Z" }, +] + [[package]] name = "protobuf" version = "7.36.1" @@ -533,6 +819,151 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "pydantic" +version = "2.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533, upload-time = "2026-08-28T09:57:35.421Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650, upload-time = "2026-08-28T09:57:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261, upload-time = "2026-08-28T09:57:39.339Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808, upload-time = "2026-08-28T09:57:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184, upload-time = "2026-08-28T09:57:42.275Z" }, + { url = "https://files.pythonhosted.org/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212, upload-time = "2026-08-28T09:57:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073, upload-time = "2026-08-28T09:57:45.613Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102, upload-time = "2026-08-28T09:57:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452, upload-time = "2026-08-28T09:57:48.362Z" }, + { url = "https://files.pythonhosted.org/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477, upload-time = "2026-08-28T09:57:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832, upload-time = "2026-08-28T09:57:51.639Z" }, + { url = "https://files.pythonhosted.org/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505, upload-time = "2026-08-28T09:57:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853, upload-time = "2026-08-28T09:57:54.592Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551, upload-time = "2026-08-28T09:57:56.017Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + [[package]] name = "pyjwt" version = "2.12.1" @@ -550,6 +981,41 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + [[package]] name = "pyopenssl" version = "26.2.0" @@ -563,6 +1029,127 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" }, ] +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rfc3161-client" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/53/e2f526fb6957023180c928448e2dd71b03b35038c315aed2d12d9a77c3af/rfc3161_client-1.0.9.tar.gz", hash = "sha256:66997415086d2c6e5d57a2d315157b32a9c3f4f1312877ca8eb55e2e0ddf8ce4", size = 112825, upload-time = "2026-09-14T19:32:29.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/2c/b81b822bc067647aa291dc4ea71f052f994344934fc84d499759aec64cf8/rfc3161_client-1.0.9-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5e619d7e28ffa3a46685923e6d6ba4ce2e74cb09a41c77995a0702cc1a533bc4", size = 2109721, upload-time = "2026-09-14T19:32:08.154Z" }, + { url = "https://files.pythonhosted.org/packages/88/cf/bcfb66eaeccdc076457451a6213360c840bcacd0e938e8507951be29373f/rfc3161_client-1.0.9-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a7004edfbf1bb7ec7801978a39fa7a7fe385ca59673df90d7578079b6eba3b24", size = 2439706, upload-time = "2026-09-14T19:32:10.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/4b69d47fd7f79238bf8db20902a30d28c908efc0d906fb57abcc89ac75e3/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b703b233dbee7228d117d5f1c118363c61d523fa6f018c9217c79e3c6684f86", size = 2651455, upload-time = "2026-09-14T19:32:12.025Z" }, + { url = "https://files.pythonhosted.org/packages/33/49/154c3e80ac94e51b9d1097dab15b95f44aa593c395bd41775e4486a7d0a0/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d3cbf1991b6f3457efcb25e1d31179cac556112e6d3e65ec9e0fd70c6e9af90d", size = 2051342, upload-time = "2026-09-14T19:32:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/c8/57/79bf6a67cd4ca729b0b74e6fa79ebef84e6bc8657b85772df01a7d3b7b89/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:614da494c6f8850c5d6d98c23bc291a4e4dbf2c3c9082d9907fa156f9e4a75c6", size = 2418228, upload-time = "2026-09-14T19:32:15.451Z" }, + { url = "https://files.pythonhosted.org/packages/11/ae/19139fee184916065705d660bad203b676dc40b449afff36f79818c59c4f/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac3f440a507555e684dc5daba75e33dc08f0f45fdefa48448f19001233a6b21", size = 2415566, upload-time = "2026-09-14T19:32:17.185Z" }, + { url = "https://files.pythonhosted.org/packages/6e/54/43f89c8e004baade51943261b29f9dd866df3383f4554d5797a2568c2855/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0ac247d5aa22f3514d1e284bdc547d2ad5f101e1e5d7f6db25db2ab3a0c59f06", size = 2995975, upload-time = "2026-09-14T19:32:19.007Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/938e4df767a2ea6fae2bc8e8e0df52e1d70a32a082a13bdfdd6fac59dc32/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:03be7ac7919c7f4c566076b07773b81b1f1b28d447fa91c6960fb5f82fac5bc1", size = 2358728, upload-time = "2026-09-14T19:32:20.644Z" }, + { url = "https://files.pythonhosted.org/packages/ff/54/f0193b27f1a635aa336d19b48c296f133719cafb87e950de6eae4638a046/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:8208999e94e1252e3f5f748f5efe59d3b9c308d04c7626be1becfbf3236c9de4", size = 2565766, upload-time = "2026-09-14T19:32:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/15/41/6c48b05fc41a424ab3b0cb0515e77046707b498837411633fe7ce77812ee/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d4cf92f0e784ff02ff5ea1c574f8521f30953dc178092424126dbd8348034f93", size = 2643597, upload-time = "2026-09-14T19:32:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/d2/18/fcb2f311b0b1754b0476165a4eae2929606007fc0ff0a575a65dbff2c0f6/rfc3161_client-1.0.9-cp39-abi3-win32.whl", hash = "sha256:4d33f5aaec2974239e1c62a79a6fcf32506b09095482377a8b5be3a00fa67dfb", size = 1996018, upload-time = "2026-09-14T19:32:25.972Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a1/b71d9f2071660a914e039061d3cdcad77b1cbccf7276bff073f95e8b4d1a/rfc3161_client-1.0.9-cp39-abi3-win_amd64.whl", hash = "sha256:908f6a775da4bdce1d39f825a4431f389542696078ef3e4ed911a068bcaf1792", size = 2431351, upload-time = "2026-09-14T19:32:27.621Z" }, +] + +[[package]] +name = "rfc8785" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/2f/fa1d2e740c490191b572d33dbca5daa180cb423c24396b856f5886371d8b/rfc8785-0.1.4.tar.gz", hash = "sha256:e545841329fe0eee4f6a3b44e7034343100c12b4ec566dc06ca9735681deb4da", size = 14321, upload-time = "2024-09-27T16:33:31.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/78/119878110660b2ad709888c8a1614fce7e2fab39080ab960656dc8605bf6/rfc8785-0.1.4-py3-none-any.whl", hash = "sha256:520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48", size = 9240, upload-time = "2024-09-27T16:33:29.683Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "securesystemslib" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/d4/f9323507144693d869f30f13694035438ab6642153b94e3057945cf0584f/securesystemslib-1.5.1.tar.gz", hash = "sha256:4b8d00abd93707ead10b69eb2b8582376a1364de3b0a71077de534c2ef4985e0", size = 947087, upload-time = "2026-09-04T06:55:32.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/c71335a5046d46bf3b761a8c7fa092b40a7721429a5613538f0a28ae80a6/securesystemslib-1.5.1-py3-none-any.whl", hash = "sha256:ada8bdf817da29ece4ba91654f6a162ce7cfbadbc3ae3f840f7313f9d22675de", size = 876390, upload-time = "2026-09-04T06:55:30.393Z" }, +] + +[[package]] +name = "sigstore" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "id" }, + { name = "importlib-resources", marker = "python_full_version < '3.11'" }, + { name = "platformdirs", version = "4.12.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "platformdirs", version = "4.13.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyasn1" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "pyopenssl" }, + { name = "requests" }, + { name = "rfc3161-client" }, + { name = "rfc8785" }, + { name = "rich" }, + { name = "sigstore-models" }, + { name = "sigstore-rekor-types" }, + { name = "tuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/e0/279419065e2d7102413605b3456122adbbccbc42e010b499c7b882fc01f8/sigstore-4.5.0.tar.gz", hash = "sha256:020d3e07f622b2916bf453e66ff6ff0711e1fdc5ab69e8bd8902f71d9fcb316f", size = 90969, upload-time = "2026-07-28T07:34:01.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/7f/51dc313c06dd3ec6c5b308e334492cfdea0b02c9184a0da7b2db8c2a30a1/sigstore-4.5.0-py3-none-any.whl", hash = "sha256:f045b207f2e12605cf775ec38e89c5eda625d71ffa7830477db65e47ec2bc8b2", size = 111724, upload-time = "2026-07-28T07:34:00.211Z" }, +] + +[[package]] +name = "sigstore-models" +version = "0.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ed/5c0ff809f90b19f4e971e17c1ed11f4df60082c6010b32a82054087e91e0/sigstore_models-0.0.6.tar.gz", hash = "sha256:c766c09470c2a7e8a4a333c893f07e2001c56a3ff1757b1a246119f53169a849", size = 7037, upload-time = "2025-11-26T19:18:12.61Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/dc/7a19864a7bc9f43121ab459e12768ab0080590e3e1b60a29b2f2eb01fb85/sigstore_models-0.0.6-py3-none-any.whl", hash = "sha256:5201a68f4d7d0f8bec1e2f4378eb646b084c52609a4e31db8c385095fff68b2e", size = 13213, upload-time = "2025-11-26T19:18:11.365Z" }, +] + +[[package]] +name = "sigstore-rekor-types" +version = "0.0.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", extra = ["email"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/54/102e772445c5e849b826fbdcd44eb9ad7b3d10fda17b08964658ec7027dc/sigstore_rekor_types-0.0.18.tar.gz", hash = "sha256:19aef25433218ebf9975a1e8b523cc84aaf3cd395ad39a30523b083ea7917ec5", size = 15687, upload-time = "2024-11-22T13:59:54.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/7c/f0b4e19fd424df4cc964f5d454e1d814fd2dc3b386342e6040441024318f/sigstore_rekor_types-0.0.18-py3-none-any.whl", hash = "sha256:b62bf38c5b1a62bc0d7fe0ee51a0709e49311d137c7880c329882a8f4b2d1d78", size = 20610, upload-time = "2024-11-22T13:59:52.684Z" }, +] + [[package]] name = "spiffe" version = "0.2.9" @@ -595,6 +1182,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/39/a5ae63a703c61ac02d04bacd5e20a3058fe6cce5ba15e7fc89b2e19b3fe6/spiffe_tls-0.3.2-py3-none-any.whl", hash = "sha256:c23d639891965b8c2047625a4959b1eeff60b540ede6355fc83f7e9c0d9e20f0", size = 26955, upload-time = "2026-05-11T15:27:36.103Z" }, ] +[[package]] +name = "tuf" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "securesystemslib" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/dd/52e7390cbac308e6b1cfe6be9bc3a96fe26326d5893cc297a52bb72792a9/tuf-7.0.1.tar.gz", hash = "sha256:5ada1db78da3518fa851e28588f4b2249b8662a28ae528f54afa2a1de541fb31", size = 274148, upload-time = "2026-09-02T06:19:52.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/10/171e306e4a19ff980b9029ce5a523881e34bc6a2a52750fb1853f49f0fb9/tuf-7.0.1-py3-none-any.whl", hash = "sha256:d30434bda6e079ab303fb30d1b3006d939a10ca34783b1573d61cd9b802fa45c", size = 56253, upload-time = "2026-09-02T06:19:50.608Z" }, +] + [[package]] name = "types-protobuf" version = "7.34.1.20260518" @@ -612,3 +1212,24 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "urllib3" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, +] diff --git a/pyproject.toml b/pyproject.toml index 221fb0f..70f2ee0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,9 @@ dependencies = [ "pyasn1>=0.6.3", "spiffe>=0.2.6", "spiffe-tls>=0.3.1", + "cryptography>=46.0.0", + "pynacl>=1.6.0", + "sigstore>=4.5.0,<5", ] [dependency-groups] @@ -49,6 +52,9 @@ packages = ["dir-sdk-python/agntcy"] pythonpath = ["dir-sdk-python"] addopts = ["--import-mode=importlib"] +[tool.coverage.run] +omit = ["*/tests/*"] + [tool.ruff.lint] extend-select = ["ANN", "ARG", "ASYNC", "B", "I", "N", "SIM"] diff --git a/renovate.json b/renovate.json index 3ac0803..85da720 100644 --- a/renovate.json +++ b/renovate.json @@ -42,7 +42,7 @@ "(?m)^\\s*#\\s*renovate:\\s*datasource=(?\\S+)\\s+depName=(?\\S+)(?:\\s+versioning=(?\\S+))?\\s*\\r?\\n\\s*[A-Z0-9_]+:\\s+'\\{\\{\\s*\\.[A-Z0-9_]+\\s*\\|\\s*default\\s+\"(?[^\"]+)\"\\s*\\}\\}'\\s*$" ], "description": [ - "Custom dependency manager for updating Directory chart, image and dirctl versions defined as Taskfile defaults." + "Custom dependency manager for updating Directory chart and image versions defined as Taskfile defaults." ] } ], @@ -191,7 +191,7 @@ "build" ], "description": [ - "Group Directory chart, image and dirctl version bumps from Taskfile in a single PR." + "Group Directory chart and image version bumps from Taskfile in a single PR." ] }, { diff --git a/uv.lock b/uv.lock index fb43d2f..d62131f 100644 --- a/uv.lock +++ b/uv.lock @@ -1,9 +1,10 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version < '3.15'", + "python_full_version >= '3.11' and python_full_version < '3.15'", + "python_full_version < '3.11'", ] [[package]] @@ -14,10 +15,13 @@ dependencies = [ { name = "agntcy-dir-grpc-python" }, { name = "agntcy-dir-protocolbuffers-python" }, { name = "authlib" }, + { name = "cryptography" }, { name = "grpcio" }, { name = "httpx" }, { name = "protobuf" }, { name = "pyasn1" }, + { name = "pynacl" }, + { name = "sigstore" }, { name = "spiffe" }, { name = "spiffe-tls" }, ] @@ -37,10 +41,13 @@ requires-dist = [ { name = "agntcy-dir-grpc-python", specifier = "==1.83.1.3.20261015113725+c24aeb0a65d8", index = "https://buf.build/gen/python" }, { name = "agntcy-dir-protocolbuffers-python", specifier = "==36.1.0.1.20261015113725+c24aeb0a65d8", index = "https://buf.build/gen/python" }, { name = "authlib", specifier = ">=1.6.9" }, + { name = "cryptography", specifier = ">=46.0.0" }, { name = "grpcio", specifier = ">=1.78.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "protobuf", specifier = ">=7.36.1" }, { name = "pyasn1", specifier = ">=0.6.3" }, + { name = "pynacl", specifier = ">=1.6.0" }, + { name = "sigstore", specifier = ">=4.5.0,<5" }, { name = "spiffe", specifier = ">=0.2.6" }, { name = "spiffe-tls", specifier = ">=0.3.1" }, ] @@ -324,6 +331,169 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/77/46e87bcfc45d25ab4db7cfc9bb544bfa3ffd302289ed31ae93f5433eb899/charset_normalizer-3.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3", size = 373348, upload-time = "2026-09-30T04:34:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/52/fc/e518013affcc43c9f919c3ba41bffe9b4ee4aceb0a6462a6243efcca5f2d/charset_normalizer-3.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e", size = 260860, upload-time = "2026-09-30T04:34:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ad/2a895c945ee61988dfd9ccee64f0b78dc09f29c9b34d1dd545246d78e0ad/charset_normalizer-3.5.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5", size = 246491, upload-time = "2026-09-30T04:34:46.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/05/5d958bc8ea503e26be25ada5430fd409cfb45dc22ff33f9f96e649613c99/charset_normalizer-3.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa", size = 290875, upload-time = "2026-09-30T04:34:47.691Z" }, + { url = "https://files.pythonhosted.org/packages/55/e2/06bad57dfdb49cad92c0ba85b6b4fa5827a67df37897287cfef0553843c2/charset_normalizer-3.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320", size = 286987, upload-time = "2026-09-30T04:34:49.356Z" }, + { url = "https://files.pythonhosted.org/packages/c4/9f/9f52d2886d52645987d603425482c63c5045a3005db1354a7097e5ed1ae9/charset_normalizer-3.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249", size = 271105, upload-time = "2026-09-30T04:34:50.957Z" }, + { url = "https://files.pythonhosted.org/packages/4a/41/e05e19578b7b87e7db2c3ecb884bd09d065274ab41e1f535542e8bae9b06/charset_normalizer-3.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e", size = 269833, upload-time = "2026-09-30T04:34:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cd/fc7152414561ff78f9c61a4a627025f65fd244abaefc99ac4e75c6169b33/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9", size = 263048, upload-time = "2026-09-30T04:34:54.013Z" }, + { url = "https://files.pythonhosted.org/packages/71/71/fb379e399b1013962716a059d551e03bd80b3e02f05d81246886af6c0958/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5", size = 249237, upload-time = "2026-09-30T04:34:55.373Z" }, + { url = "https://files.pythonhosted.org/packages/33/4f/aeadccd6d20882909eb2597ec40eccc05d505e2262f14dc76d7620700657/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c", size = 290756, upload-time = "2026-09-30T04:34:56.867Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7b/e8a92613236b257d3b75d532496eb21f68ad8231d7d56df94497e460112e/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c", size = 269026, upload-time = "2026-09-30T04:34:58.278Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ac/21d5c6b972285c5f095ff78afdc99f3539e3985dfcf7ff1cfbe9e772f529/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be", size = 287601, upload-time = "2026-09-30T04:34:59.703Z" }, + { url = "https://files.pythonhosted.org/packages/59/72/263491ec1494a194b16fcbff88a0220f2af633738c79e92b4d7189226322/charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45", size = 272660, upload-time = "2026-09-30T04:35:01.26Z" }, + { url = "https://files.pythonhosted.org/packages/38/7a/81f1186c57e406233969ae96b5fd0c907254ab8b6e47da8ae24cd20fb9d7/charset_normalizer-3.5.2-cp310-cp310-win32.whl", hash = "sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4", size = 187301, upload-time = "2026-09-30T04:35:02.659Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/fb8b5178fdf7bf68efa61cbe892286bc9f73846f26147d4725fb89388475/charset_normalizer-3.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275", size = 215446, upload-time = "2026-09-30T04:35:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/36/40/e270b74e0d3f583d78d902960845a149e80eb7c737ee5613ccc6efc7264b/charset_normalizer-3.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c", size = 203389, upload-time = "2026-09-30T04:35:05.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/67/6a0b94a7960d5e1b5eacd2fb529f3fccc47db4644f7f0a7cfdcfc3be578a/charset_normalizer-3.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6", size = 370642, upload-time = "2026-09-30T04:35:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/01009e13b94041599004edf32e56e382c24e570f60f79bab8efe45cfe1eb/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5", size = 259280, upload-time = "2026-09-30T04:35:08.448Z" }, + { url = "https://files.pythonhosted.org/packages/66/85/3b5358f60a13210f0b67d3755c168ef758701b021e655d88d4da28554467/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74", size = 245335, upload-time = "2026-09-30T04:35:10.104Z" }, + { url = "https://files.pythonhosted.org/packages/74/75/77c1c479b09ecd751d1e767b251ea5c14d4d50ff757bf404afab2692f600/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab", size = 291707, upload-time = "2026-09-30T04:35:11.575Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0d/363f78cacb70f58f15f4b083961bbd9d292f335d3f5c66fc4f1cfe69cb90/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c", size = 286747, upload-time = "2026-09-30T04:35:13.022Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ed/cf505d3011ffceb12c2067a7a5d3cfe92b875d4d44bb0ff0d69375e2c184/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f", size = 269972, upload-time = "2026-09-30T04:35:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/15/d8/f0a93a431d170e7ca681d4f6650fee3de934d18560e474e7267eb4b0f987/charset_normalizer-3.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288", size = 269339, upload-time = "2026-09-30T04:35:16.087Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/9b2bd1c5b7af02462c9752d33994834ff972a96b4c483eefde9e594488e2/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400", size = 261683, upload-time = "2026-09-30T04:35:17.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/a5/cac540ab0fd61f3fec88ad3dbb64509e71424593d73cfdfff5ab3e4db279/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd", size = 248143, upload-time = "2026-09-30T04:35:18.849Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/ff467301deef2089fad87f72df9e000a26a78fec7acbb18e1999371b8369/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37", size = 292358, upload-time = "2026-09-30T04:35:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/22d7e785d1e210afc2e2f58600dd1799d17a35665faf84383f002826c5f8/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac", size = 268766, upload-time = "2026-09-30T04:35:21.72Z" }, + { url = "https://files.pythonhosted.org/packages/ae/91/e8e946267f1c2d9e2bd651726e2fbd2addf02c4d36cea5069e32ca9d7bb5/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a", size = 287432, upload-time = "2026-09-30T04:35:23.273Z" }, + { url = "https://files.pythonhosted.org/packages/4e/88/7561d8a88d555e7df6623abe7c0070b4baf47549b9408783a2ae0a1a6cf7/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640", size = 272470, upload-time = "2026-09-30T04:35:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/35/7e/578c702301ec036f01455f30744a08d2b42f6ab35b9b2d4bf8cae0ef2a80/charset_normalizer-3.5.2-cp311-cp311-win32.whl", hash = "sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d", size = 186688, upload-time = "2026-09-30T04:35:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/e8/fc/fdf8cf52ff21cd5bf158f20978991cf985325842f74283eb6df26c8a39d8/charset_normalizer-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96", size = 214932, upload-time = "2026-09-30T04:35:27.796Z" }, + { url = "https://files.pythonhosted.org/packages/97/66/3e45a506d8110b632541faf9a9470185aa9878f1ed44020f31346c1c5e5b/charset_normalizer-3.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1", size = 202828, upload-time = "2026-09-30T04:35:29.259Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/693809898870237d82785a03f3b2b58fe4c9f14669f84a7d4e623c92a59e/charset_normalizer-3.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491", size = 367780, upload-time = "2026-09-30T04:35:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/c9/87/2fea8c13dc24b3ca9c6f803a5b2dfdeae73eb4f9e12c7885ed908ff0433c/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c", size = 246730, upload-time = "2026-09-30T04:35:32.286Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9e/09efac30b937722f46d3110ba30b875b24b2e3a266ed746cc4e376a94d80/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0", size = 237707, upload-time = "2026-09-30T04:35:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/9e/18/70d76670b13686237863a379928d60bd10e021f17d243ab3d7014c4a5f4e/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51", size = 273050, upload-time = "2026-09-30T04:35:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/54/e2/77a8b09d5adc013ed07b95b01b8b8fa5441c4e810e83ee7e4aae2fa4d91a/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5", size = 270345, upload-time = "2026-09-30T04:35:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c5/38806a25ab5e65fc178f39affeda20858efafede2fce1ffc2556cfc9fe73/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649", size = 257601, upload-time = "2026-09-30T04:35:37.919Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/213565184708fdb263ae55e2c04ee1ff748129dd65d48ed0e3502da9c85a/charset_normalizer-3.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e", size = 252222, upload-time = "2026-09-30T04:35:39.544Z" }, + { url = "https://files.pythonhosted.org/packages/7e/24/76d2cefc25472531e4c5c7dfff68865eb1c39b78482f0fdc15b46f047830/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346", size = 248482, upload-time = "2026-09-30T04:35:41.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/dc/65a801b66ab4c197e22c433ab25e7ac24324ac6f45a2269aca42cce309bf/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1", size = 241206, upload-time = "2026-09-30T04:35:42.59Z" }, + { url = "https://files.pythonhosted.org/packages/a7/95/ca9b5eabde673002c6f1e7ada1b223916fe18f6d661da7aabd4d643718f1/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875", size = 273190, upload-time = "2026-09-30T04:35:44.347Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/803b4d2a3f6e1740f63f1e87b04d14b42f3d4fdfe6ed7d4db2d34102b14f/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1", size = 253527, upload-time = "2026-09-30T04:35:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/55/93c0e5dbd085ae0471346026abbe7e0db9ea2d6fea74e51f0b5a46f233a7/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413", size = 271285, upload-time = "2026-09-30T04:35:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/95/69/0dbd0e0b9b16cfa816cdfcb3e2e3854a1f680dc07fb1245ea125e7448060/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869", size = 260010, upload-time = "2026-09-30T04:35:48.996Z" }, + { url = "https://files.pythonhosted.org/packages/58/9d/e7b88e7b1bf403590c3b573277b5e1e488c68c7a6fbacca310a2c324e90c/charset_normalizer-3.5.2-cp312-cp312-win32.whl", hash = "sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e", size = 184125, upload-time = "2026-09-30T04:35:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/e6e083884cbcfd49c64865af05027fe7011be7b2d9179524f099a1b611f3/charset_normalizer-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc", size = 207486, upload-time = "2026-09-30T04:35:52.194Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e3/017aea0911ada7405a825c7d937eb3a13009664e2f5b38e8c4bbf2abf894/charset_normalizer-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3", size = 196734, upload-time = "2026-09-30T04:35:53.636Z" }, + { url = "https://files.pythonhosted.org/packages/c5/34/68292d68512768591aaff07c59bb53ee31341c87759433a859c4641a50c2/charset_normalizer-3.5.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5", size = 225946, upload-time = "2026-09-30T04:35:55.313Z" }, + { url = "https://files.pythonhosted.org/packages/e3/80/bee0b01b90ccd5322ae1d0abb33fab1bd95b7c2eadaf02aeccf22e04ee83/charset_normalizer-3.5.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e", size = 238619, upload-time = "2026-09-30T04:35:56.863Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/60ce52a85a7fd631ae8482ae6d74521014ca2f255892679484dc04d7ef56/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a", size = 206701, upload-time = "2026-09-30T04:35:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/36/8c/71aafad23f971afc84c2b295bc0c560739ce1dac558aad9fec22e39f3639/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d", size = 210506, upload-time = "2026-09-30T04:36:00.147Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/3c5a7798c046df7d2d68ad653cf5b6c5a8bfee225055a843c6f2f42aac1a/charset_normalizer-3.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055", size = 366626, upload-time = "2026-09-30T04:36:01.77Z" }, + { url = "https://files.pythonhosted.org/packages/e1/16/710ac3de2ee354e2bd1a9c94efe45a2d27b5c6ad39b2d6a905be2c094b6c/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858", size = 244624, upload-time = "2026-09-30T04:36:03.389Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/45c7439f5b63d24f7d5b2a1d760f34af7628782d7144b4cc8ded45c2d4bc/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234", size = 235731, upload-time = "2026-09-30T04:36:04.987Z" }, + { url = "https://files.pythonhosted.org/packages/4d/34/38f3154785ce92e9f56eb226f4d35bdfae6b008480dd055f58837a89c810/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21", size = 270352, upload-time = "2026-09-30T04:36:06.412Z" }, + { url = "https://files.pythonhosted.org/packages/04/f3/859f74e7babc977705026b30593b3be04049632a522fb7000f83c033d747/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718", size = 267393, upload-time = "2026-09-30T04:36:07.865Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/41d27f234b82e47c167a5f6c0f62501dc0c640585ff4aba79e08a390336a/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4", size = 254660, upload-time = "2026-09-30T04:36:09.248Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/5d1a997587febe5b26d8daffe363b5c1a091cece19828eec6502fd09c5ef/charset_normalizer-3.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3", size = 249996, upload-time = "2026-09-30T04:36:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1f/d1e78246f7ed60c8c8d606b4ac27f66ce49cc3e95f24893ccbeba9f77302/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c", size = 247028, upload-time = "2026-09-30T04:36:12.294Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/eba316edd4f0c4d3a5d945924c4eeeae59abac4056aa815d8a4268f863a2/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429", size = 240562, upload-time = "2026-09-30T04:36:13.887Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8e/aaa037d40ca9ef045977f1a661048b1aa33f223adfce3452fe9be9f79d14/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f", size = 271699, upload-time = "2026-09-30T04:36:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/26/19/1c1c9f75974adf523b87f34b8a2adc5a435cd65916812bcbd0dfa45f9a29/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a", size = 252030, upload-time = "2026-09-30T04:36:16.839Z" }, + { url = "https://files.pythonhosted.org/packages/bc/90/0660ef18e18df0a4d2a1a0edff7dfbba42d4e50ef2425557a5bb7051f77b/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00", size = 268565, upload-time = "2026-09-30T04:36:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/79/ba/57adc269824e8658f1a0f97a9e514c247445a9632b3419b97e0ba37f16dc/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d", size = 257627, upload-time = "2026-09-30T04:36:19.938Z" }, + { url = "https://files.pythonhosted.org/packages/9a/85/33abd4315c052d3d4f54c92b1ee49bfbc0dc7115a981e462a793b6d2ab87/charset_normalizer-3.5.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3", size = 143407, upload-time = "2026-09-30T04:36:21.376Z" }, + { url = "https://files.pythonhosted.org/packages/4f/de/6435e18d1aaa5d910b896d551411c96af1f42a0c56c29afc2016c61ccc2e/charset_normalizer-3.5.2-cp313-cp313-win32.whl", hash = "sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd", size = 183207, upload-time = "2026-09-30T04:36:22.776Z" }, + { url = "https://files.pythonhosted.org/packages/9c/76/b8ec57f4e9ee3253541abf95e4a462c0175fe8032dcd070f1f2421240942/charset_normalizer-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639", size = 206489, upload-time = "2026-09-30T04:36:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/c647c6ae47480221e875ea5d743ff94946f7416e3c69415ab772928e8d32/charset_normalizer-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3", size = 195501, upload-time = "2026-09-30T04:36:25.846Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" }, + { url = "https://files.pythonhosted.org/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" }, + { url = "https://files.pythonhosted.org/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" }, + { url = "https://files.pythonhosted.org/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" }, + { url = "https://files.pythonhosted.org/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" }, + { url = "https://files.pythonhosted.org/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" }, + { url = "https://files.pythonhosted.org/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" }, + { url = "https://files.pythonhosted.org/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" }, + { url = "https://files.pythonhosted.org/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" }, + { url = "https://files.pythonhosted.org/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" }, + { url = "https://files.pythonhosted.org/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" }, + { url = "https://files.pythonhosted.org/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" }, + { url = "https://files.pythonhosted.org/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" }, + { url = "https://files.pythonhosted.org/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", size = 367676, upload-time = "2026-09-30T04:37:28.329Z" }, + { url = "https://files.pythonhosted.org/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", size = 248414, upload-time = "2026-09-30T04:37:30.167Z" }, + { url = "https://files.pythonhosted.org/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", size = 244775, upload-time = "2026-09-30T04:37:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", size = 273877, upload-time = "2026-09-30T04:37:33.661Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", size = 269455, upload-time = "2026-09-30T04:37:35.573Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", size = 257336, upload-time = "2026-09-30T04:37:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", size = 257584, upload-time = "2026-09-30T04:37:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", size = 251854, upload-time = "2026-09-30T04:37:41.23Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", size = 248767, upload-time = "2026-09-30T04:37:42.883Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", size = 274479, upload-time = "2026-09-30T04:37:44.658Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", size = 258310, upload-time = "2026-09-30T04:37:46.529Z" }, + { url = "https://files.pythonhosted.org/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", size = 271100, upload-time = "2026-09-30T04:37:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", size = 259936, upload-time = "2026-09-30T04:37:50.023Z" }, + { url = "https://files.pythonhosted.org/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", size = 186301, upload-time = "2026-09-30T04:37:51.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", size = 210858, upload-time = "2026-09-30T04:37:53.427Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", size = 200309, upload-time = "2026-09-30T04:37:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", size = 395343, upload-time = "2026-09-30T04:37:56.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", size = 250114, upload-time = "2026-09-30T04:37:58.607Z" }, + { url = "https://files.pythonhosted.org/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", size = 239997, upload-time = "2026-09-30T04:38:00.511Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", size = 272313, upload-time = "2026-09-30T04:38:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", size = 269986, upload-time = "2026-09-30T04:38:04.435Z" }, + { url = "https://files.pythonhosted.org/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", size = 260202, upload-time = "2026-09-30T04:38:06.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", size = 259083, upload-time = "2026-09-30T04:38:08.311Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", size = 253081, upload-time = "2026-09-30T04:38:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", size = 248712, upload-time = "2026-09-30T04:38:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", size = 274176, upload-time = "2026-09-30T04:38:14.138Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", size = 259413, upload-time = "2026-09-30T04:38:15.841Z" }, + { url = "https://files.pythonhosted.org/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", size = 269434, upload-time = "2026-09-30T04:38:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", size = 262716, upload-time = "2026-09-30T04:38:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", size = 197159, upload-time = "2026-09-30T04:38:21.037Z" }, + { url = "https://files.pythonhosted.org/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", size = 221322, upload-time = "2026-09-30T04:38:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", size = 209192, upload-time = "2026-09-30T04:38:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" }, + { url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -493,12 +663,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "dnspython" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.11' and python_full_version < '3.15'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4a/50822184bd67cc6493f0fb6a880749158fcd31ab3fa07409acfd91f9fc85/dnspython-2.9.0.tar.gz", hash = "sha256:b44dc6b18f07a8b1c56676a19fbfdb5209415b046a9cece286baafa87ff3f7f1", size = 423560, upload-time = "2026-10-09T00:07:24.352Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/02/cdcc9b7c051786a103c3b09e1003a82fa0c66bcb91ffbdabcfbf7b4163b9/dnspython-2.9.0-py3-none-any.whl", hash = "sha256:9a4aedb833c3c1b49214d04d44d3032ab7a9135f7c1d29a549b4ff78fd82fda9", size = 354822, upload-time = "2026-10-09T00:07:22.622Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "dnspython", version = "2.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -639,6 +848,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "id" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, +] + [[package]] name = "idna" version = "3.18" @@ -648,6 +869,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] +[[package]] +name = "importlib-resources" +version = "5.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/f1/8711c49ffd121083007a24c1bff0d324c9ff621d4fdf8b4ffcb8d9e60330/importlib_resources-5.13.0.tar.gz", hash = "sha256:82d5c6cca930697dbbd86c93333bb2c2e72861d4789a11c2662b933e5ad2b528", size = 36550, upload-time = "2023-07-07T16:21:00.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/68/bd9dd6bbf06772c7accce77d0354d783333fbe712a60b08fc13540c05422/importlib_resources-5.13.0-py3-none-any.whl", hash = "sha256:9f7bd0c97b79972a6cce36a366356d16d5e13b09679c11a58f1014bfdf8e64b2", size = 32912, upload-time = "2023-07-07T16:20:58.128Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -799,6 +1029,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mypy" version = "2.4.0" @@ -902,6 +1153,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/97/8299a481ae6c08494b5d53511e6a4746775d8a354c685c69d8796b2ed482/pem-23.1.0-py3-none-any.whl", hash = "sha256:78bbb1e75b737891350cb9499cbba31da5d59545f360f44163c0bc751cad55d3", size = 9195, upload-time = "2023-06-21T10:24:39.164Z" }, ] +[[package]] +name = "platformdirs" +version = "4.12.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/90/a1/d5f9002a70298c64a789779077d8dd90c10aa1f47fe40c86802df874f2a6/platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250", size = 60010, upload-time = "2026-10-07T23:30:32.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/ba/223e00b885e960edd5d4b4d178c88acce019bd5b83786093681b8c393492/platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7", size = 32596, upload-time = "2026-10-07T23:30:30.825Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.11' and python_full_version < '3.15'", +] +sdist = { url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", size = 62362, upload-time = "2026-10-19T01:02:11.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/8a/1d8cb3842909dc193a73835adedf412e32e18f8ffd59f43983949622fe6f/platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e", size = 32779, upload-time = "2026-10-19T01:02:10.17Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -971,6 +1247,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.46.4" @@ -1113,6 +1394,41 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + [[package]] name = "pyopenssl" version = "26.4.0" @@ -1158,6 +1474,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rfc3161-client" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/53/e2f526fb6957023180c928448e2dd71b03b35038c315aed2d12d9a77c3af/rfc3161_client-1.0.9.tar.gz", hash = "sha256:66997415086d2c6e5d57a2d315157b32a9c3f4f1312877ca8eb55e2e0ddf8ce4", size = 112825, upload-time = "2026-09-14T19:32:29.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/2c/b81b822bc067647aa291dc4ea71f052f994344934fc84d499759aec64cf8/rfc3161_client-1.0.9-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5e619d7e28ffa3a46685923e6d6ba4ce2e74cb09a41c77995a0702cc1a533bc4", size = 2109721, upload-time = "2026-09-14T19:32:08.154Z" }, + { url = "https://files.pythonhosted.org/packages/88/cf/bcfb66eaeccdc076457451a6213360c840bcacd0e938e8507951be29373f/rfc3161_client-1.0.9-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a7004edfbf1bb7ec7801978a39fa7a7fe385ca59673df90d7578079b6eba3b24", size = 2439706, upload-time = "2026-09-14T19:32:10.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/4b69d47fd7f79238bf8db20902a30d28c908efc0d906fb57abcc89ac75e3/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b703b233dbee7228d117d5f1c118363c61d523fa6f018c9217c79e3c6684f86", size = 2651455, upload-time = "2026-09-14T19:32:12.025Z" }, + { url = "https://files.pythonhosted.org/packages/33/49/154c3e80ac94e51b9d1097dab15b95f44aa593c395bd41775e4486a7d0a0/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d3cbf1991b6f3457efcb25e1d31179cac556112e6d3e65ec9e0fd70c6e9af90d", size = 2051342, upload-time = "2026-09-14T19:32:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/c8/57/79bf6a67cd4ca729b0b74e6fa79ebef84e6bc8657b85772df01a7d3b7b89/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:614da494c6f8850c5d6d98c23bc291a4e4dbf2c3c9082d9907fa156f9e4a75c6", size = 2418228, upload-time = "2026-09-14T19:32:15.451Z" }, + { url = "https://files.pythonhosted.org/packages/11/ae/19139fee184916065705d660bad203b676dc40b449afff36f79818c59c4f/rfc3161_client-1.0.9-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac3f440a507555e684dc5daba75e33dc08f0f45fdefa48448f19001233a6b21", size = 2415566, upload-time = "2026-09-14T19:32:17.185Z" }, + { url = "https://files.pythonhosted.org/packages/6e/54/43f89c8e004baade51943261b29f9dd866df3383f4554d5797a2568c2855/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0ac247d5aa22f3514d1e284bdc547d2ad5f101e1e5d7f6db25db2ab3a0c59f06", size = 2995975, upload-time = "2026-09-14T19:32:19.007Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/938e4df767a2ea6fae2bc8e8e0df52e1d70a32a082a13bdfdd6fac59dc32/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:03be7ac7919c7f4c566076b07773b81b1f1b28d447fa91c6960fb5f82fac5bc1", size = 2358728, upload-time = "2026-09-14T19:32:20.644Z" }, + { url = "https://files.pythonhosted.org/packages/ff/54/f0193b27f1a635aa336d19b48c296f133719cafb87e950de6eae4638a046/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:8208999e94e1252e3f5f748f5efe59d3b9c308d04c7626be1becfbf3236c9de4", size = 2565766, upload-time = "2026-09-14T19:32:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/15/41/6c48b05fc41a424ab3b0cb0515e77046707b498837411633fe7ce77812ee/rfc3161_client-1.0.9-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d4cf92f0e784ff02ff5ea1c574f8521f30953dc178092424126dbd8348034f93", size = 2643597, upload-time = "2026-09-14T19:32:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/d2/18/fcb2f311b0b1754b0476165a4eae2929606007fc0ff0a575a65dbff2c0f6/rfc3161_client-1.0.9-cp39-abi3-win32.whl", hash = "sha256:4d33f5aaec2974239e1c62a79a6fcf32506b09095482377a8b5be3a00fa67dfb", size = 1996018, upload-time = "2026-09-14T19:32:25.972Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a1/b71d9f2071660a914e039061d3cdcad77b1cbccf7276bff073f95e8b4d1a/rfc3161_client-1.0.9-cp39-abi3-win_amd64.whl", hash = "sha256:908f6a775da4bdce1d39f825a4431f389542696078ef3e4ed911a068bcaf1792", size = 2431351, upload-time = "2026-09-14T19:32:27.621Z" }, +] + +[[package]] +name = "rfc8785" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/2f/fa1d2e740c490191b572d33dbca5daa180cb423c24396b856f5886371d8b/rfc8785-0.1.4.tar.gz", hash = "sha256:e545841329fe0eee4f6a3b44e7034343100c12b4ec566dc06ca9735681deb4da", size = 14321, upload-time = "2024-09-27T16:33:31.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/78/119878110660b2ad709888c8a1614fce7e2fab39080ab960656dc8605bf6/rfc8785-0.1.4-py3-none-any.whl", hash = "sha256:520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48", size = 9240, upload-time = "2024-09-27T16:33:29.683Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "ruff" version = "0.16.10" @@ -1183,6 +1559,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, ] +[[package]] +name = "securesystemslib" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/d4/f9323507144693d869f30f13694035438ab6642153b94e3057945cf0584f/securesystemslib-1.5.1.tar.gz", hash = "sha256:4b8d00abd93707ead10b69eb2b8582376a1364de3b0a71077de534c2ef4985e0", size = 947087, upload-time = "2026-09-04T06:55:32.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/c71335a5046d46bf3b761a8c7fa092b40a7721429a5613538f0a28ae80a6/securesystemslib-1.5.1-py3-none-any.whl", hash = "sha256:ada8bdf817da29ece4ba91654f6a162ce7cfbadbc3ae3f840f7313f9d22675de", size = 876390, upload-time = "2026-09-04T06:55:30.393Z" }, +] + +[[package]] +name = "sigstore" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "id" }, + { name = "importlib-resources", marker = "python_full_version < '3.11'" }, + { name = "platformdirs", version = "4.12.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "platformdirs", version = "4.13.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyasn1" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "pyopenssl" }, + { name = "requests" }, + { name = "rfc3161-client" }, + { name = "rfc8785" }, + { name = "rich" }, + { name = "sigstore-models" }, + { name = "sigstore-rekor-types" }, + { name = "tuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/e0/279419065e2d7102413605b3456122adbbccbc42e010b499c7b882fc01f8/sigstore-4.5.0.tar.gz", hash = "sha256:020d3e07f622b2916bf453e66ff6ff0711e1fdc5ab69e8bd8902f71d9fcb316f", size = 90969, upload-time = "2026-07-28T07:34:01.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/7f/51dc313c06dd3ec6c5b308e334492cfdea0b02c9184a0da7b2db8c2a30a1/sigstore-4.5.0-py3-none-any.whl", hash = "sha256:f045b207f2e12605cf775ec38e89c5eda625d71ffa7830477db65e47ec2bc8b2", size = 111724, upload-time = "2026-07-28T07:34:00.211Z" }, +] + +[[package]] +name = "sigstore-models" +version = "0.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ed/5c0ff809f90b19f4e971e17c1ed11f4df60082c6010b32a82054087e91e0/sigstore_models-0.0.6.tar.gz", hash = "sha256:c766c09470c2a7e8a4a333c893f07e2001c56a3ff1757b1a246119f53169a849", size = 7037, upload-time = "2025-11-26T19:18:12.61Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/dc/7a19864a7bc9f43121ab459e12768ab0080590e3e1b60a29b2f2eb01fb85/sigstore_models-0.0.6-py3-none-any.whl", hash = "sha256:5201a68f4d7d0f8bec1e2f4378eb646b084c52609a4e31db8c385095fff68b2e", size = 13213, upload-time = "2025-11-26T19:18:11.365Z" }, +] + +[[package]] +name = "sigstore-rekor-types" +version = "0.0.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", extra = ["email"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/54/102e772445c5e849b826fbdcd44eb9ad7b3d10fda17b08964658ec7027dc/sigstore_rekor_types-0.0.18.tar.gz", hash = "sha256:19aef25433218ebf9975a1e8b523cc84aaf3cd395ad39a30523b083ea7917ec5", size = 15687, upload-time = "2024-11-22T13:59:54.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/7c/f0b4e19fd424df4cc964f5d454e1d814fd2dc3b386342e6040441024318f/sigstore_rekor_types-0.0.18-py3-none-any.whl", hash = "sha256:b62bf38c5b1a62bc0d7fe0ee51a0709e49311d137c7880c329882a8f4b2d1d78", size = 20610, upload-time = "2024-11-22T13:59:52.684Z" }, +] + [[package]] name = "spiffe" version = "0.3.0" @@ -1269,6 +1706,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "tuf" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "securesystemslib" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/dd/52e7390cbac308e6b1cfe6be9bc3a96fe26326d5893cc297a52bb72792a9/tuf-7.0.1.tar.gz", hash = "sha256:5ada1db78da3518fa851e28588f4b2249b8662a28ae528f54afa2a1de541fb31", size = 274148, upload-time = "2026-09-02T06:19:52.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/10/171e306e4a19ff980b9029ce5a523881e34bc6a2a52750fb1853f49f0fb9/tuf-7.0.1-py3-none-any.whl", hash = "sha256:d30434bda6e079ab303fb30d1b3006d939a10ca34783b1573d61cd9b802fa45c", size = 56253, upload-time = "2026-09-02T06:19:50.608Z" }, +] + [[package]] name = "types-protobuf" version = "7.34.1.20260518" @@ -1299,6 +1749,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "urllib3" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, +] + [[package]] name = "uuid" version = "1.30"