diff --git a/.github/workflows/ci-quality.yml b/.github/workflows/ci-quality.yml index f8083f007..243dac9de 100644 --- a/.github/workflows/ci-quality.yml +++ b/.github/workflows/ci-quality.yml @@ -21,6 +21,19 @@ on: - 'uv.lock' - '.ruff.toml' - '.github/workflows/ci-quality.yml' + # pull_request_target is needed only for the auto-format-suggest job to call + # the PR Reviews API on fork PRs. All other jobs gate on event_name to avoid + # double execution. + pull_request_target: + branches: [ main, develop ] + paths: + - 'src/**' + - 'tests/**' + - 'pyproject.toml' + - 'requirements*.txt' + - 'uv.lock' + - '.ruff.toml' + - '.github/workflows/ci-quality.yml' permissions: contents: read @@ -32,6 +45,8 @@ jobs: quality-check: name: Quality Standards + # Skip on pull_request_target — only auto-format-suggest needs that event. + if: github.event_name != 'pull_request_target' runs-on: ubuntu-latest needs: config permissions: @@ -65,6 +80,8 @@ jobs: setup-cache: name: Setup Cache + # Runs on both pull_request and pull_request_target — auto-format-suggest + # depends on it and needs to run on pull_request_target for fork PRs. needs: config uses: ./.github/workflows/cache-management.yml with: @@ -73,8 +90,10 @@ jobs: python-version: ${{ needs.config.outputs.default-python-version }} auto-format: - name: Auto-Format Code - if: github.event_name == 'pull_request' + name: Auto-Format Code + # Same-repo PRs only: GITHUB_TOKEN can push directly to the branch. + # Fork PRs are handled by the auto-format-suggest job below. + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest needs: [config, setup-cache] permissions: @@ -108,8 +127,48 @@ jobs: git push fi + auto-format-suggest: + name: Auto-Format Suggestions + # Fork PRs: post formatted changes as PR review suggestions for the + # contributor to apply via the GitHub UI's "Commit suggestion" button. + # Uses pull_request_target so the GITHUB_TOKEN can call the PR Reviews API + # (POST /pulls/{id}/reviews) — pull_request fork-PR tokens cannot. + # The workflow file is evaluated at the base ref (safe); only the PR head + # code is checked out into a non-elevated working tree for formatting. + if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + needs: [config, setup-cache] + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout PR head + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + # No token here — formatter does not need write access; this prevents + # untrusted PR code from authenticating with elevated permissions. + persist-credentials: false + + - name: Setup UV with cache + uses: ./.github/actions/setup-uv-cached + with: + cache-key: ${{ needs.setup-cache.outputs.cache-key }} + fail-on-cache-miss: false + + - name: Run formatter + run: make format-fix + + - name: Post review suggestions + uses: reviewdog/action-suggester@aa38384ceb608d00f84b4690cacc83a5aba307ff # v1.24.0 + with: + tool_name: ruff + level: warning + fail_on_error: false + lint-ruff: name: Ruff (Code Quality) + if: github.event_name != 'pull_request_target' runs-on: ubuntu-latest needs: [config, setup-cache] permissions: @@ -129,6 +188,7 @@ jobs: lint-ruff-optional: name: Ruff (Extended Checks) + if: github.event_name != 'pull_request_target' runs-on: ubuntu-latest needs: [config, setup-cache, lint-ruff] permissions: @@ -149,6 +209,7 @@ jobs: lint-pyright: name: Type Checking (pyright) + if: github.event_name != 'pull_request_target' runs-on: ubuntu-latest needs: [config, setup-cache, lint-ruff] permissions: @@ -169,6 +230,7 @@ jobs: arch-validation: name: Architecture Validation + if: github.event_name != 'pull_request_target' runs-on: ubuntu-latest needs: [config, setup-cache, lint-ruff] permissions: diff --git a/.github/workflows/security-code.yml b/.github/workflows/security-code.yml index a38a4d99b..41fb58035 100644 --- a/.github/workflows/security-code.yml +++ b/.github/workflows/security-code.yml @@ -118,12 +118,17 @@ jobs: # in their PyPI METADATA. The compound string is treated as a single # non-allowlisted license. Each exemption below has been verified by reading # the package's METADATA license / License-Expression directly: - # werkzeug BSD-3-Clause AND LicenseRef-scancode-unknown-license-reference -> BSD-3-Clause - # lark MIT AND MPL-2.0 -> MIT (METADATA: License: MIT) - # mcp MIT AND Python-2.0 -> MIT (METADATA: License: MIT) - # rfc3987-syntax Apache-2.0 AND GPL-1.0-or-later AND MIT -> MIT (METADATA: License-Expression: MIT) - # wcwidth MIT AND HPND-Markus-Kuhn -> MIT (METADATA: License-Expression: MIT) - allow-dependencies-licenses: 'pkg:pypi/werkzeug, pkg:githubactions/trufflesecurity/trufflehog, pkg:pypi/lark, pkg:pypi/mcp, pkg:pypi/rfc3987-syntax, pkg:pypi/wcwidth' + # werkzeug BSD-3-Clause AND LicenseRef-scancode-unknown-license-reference -> BSD-3-Clause + # lark MIT AND MPL-2.0 -> MIT (METADATA: License: MIT) + # mcp MIT AND Python-2.0 -> MIT (METADATA: License: MIT) + # rfc3987-syntax Apache-2.0 AND GPL-1.0-or-later AND MIT -> MIT (METADATA: License-Expression: MIT) + # wcwidth MIT AND HPND-Markus-Kuhn -> MIT (METADATA: License-Expression: MIT) + # lxml BSD-3-Clause AND GPL-1.0-or-later -> BSD-3-Clause (METADATA: License: BSD-3-Clause; GPL applies to a single legacy source file) + # protobuf BSD-3-Clause AND LicenseRef-scancode-protobuf -> 3-Clause BSD (METADATA License text) + # pywin32 PSF-2.0 -> PSF-2.0 (permissive, equivalent to BSD) + # cyclonedx-python-lib no detected license -> Apache-2.0 (METADATA: License: Apache-2.0) + # fastapi no detected license -> MIT (METADATA: License-Expression: MIT) + allow-dependencies-licenses: 'pkg:pypi/werkzeug, pkg:githubactions/trufflesecurity/trufflehog, pkg:pypi/lark, pkg:pypi/mcp, pkg:pypi/rfc3987-syntax, pkg:pypi/wcwidth, pkg:pypi/lxml, pkg:pypi/protobuf, pkg:pypi/pywin32, pkg:pypi/cyclonedx-python-lib, pkg:pypi/fastapi' codeql-analysis: name: CodeQL Analysis diff --git a/.github/workflows/validate-workflows.yml b/.github/workflows/validate-workflows.yml index 8ad0f71dd..6523d8cf2 100644 --- a/.github/workflows/validate-workflows.yml +++ b/.github/workflows/validate-workflows.yml @@ -69,4 +69,4 @@ jobs: - name: Validate shell scripts with shellcheck uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 with: - ignore_paths: '.venv .git __pycache__ node_modules' + ignore_paths: '.venv .git __pycache__ node_modules k8s' diff --git a/.gitignore b/.gitignore index 9dfe59b4d..90f4995ef 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,8 @@ requirements-dev.txt work/ # Test autogenerated files -tests/onaws/run_templates* +tests/providers/aws/live/run_templates/ +tests/providers/aws/live/run_templates/** # Desloppify scan artifacts .desloppify/ diff --git a/.trivyignore b/.trivyignore index 19fe66b57..98f8ede74 100644 --- a/.trivyignore +++ b/.trivyignore @@ -16,3 +16,9 @@ __pycache__/** htmlcov/** .tox/** .mypy_cache/** + +# CVE exceptions for transitive CI/dev-only dependencies with no upstream patch +# nltk 3.9.4 — CVE-2026-54293 (HIGH). Pulled in transitively by `safety` (CI-only +# security scanner). No fixed version available upstream as of 2026-06-17. Not +# present in runtime install. Re-evaluate when nltk publishes a patched release. +CVE-2026-54293 diff --git a/CHANGELOG.md b/CHANGELOG.md index 44249cb97..e7ea52a97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,36 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased +### Added + +- New `[aws]` extra (alias for AWS deps currently in core). The canonical install + command going forward is `pip install orb-py[aws]`. This continues to work + unchanged when AWS deps move out of core in a future major version. +- New `[all-providers]` meta-extra: pulls in all currently implemented providers. +- New `[monitoring-aws]` extra: AWS-specific OpenTelemetry boto instrumentation (previously bundled inside `[monitoring]`). +- New `[test-aws]` extra: moto + response-mocking deps for AWS test suites. +- Architecture test `test_boto3_leak_detection.py`: asserts boto3/botocore are never imported outside `providers/aws/` except by guarded backward-compat shims. +- Unit tests `test_no_provider_install.py`: verifies ORB core modules boot cleanly when AWS deps are absent. + +### Changed + +- `[monitoring]` extra no longer includes `opentelemetry-instrumentation-boto` (use `[monitoring-aws]`). +- `[all]` extra now includes `[all-providers]` so `pip install orb-py[all]` still pulls everything. +- `[dev]` shim extra now includes `[all-providers]` for full local development. +- Three module-level import sites guarded with `try/except ImportError` so ORB core boots when AWS extras are absent: + `config/schemas/cleanup_schema.py`, `infrastructure/storage/registration.py`, + `providers/registration.py` (deprecated shims). +- `tests/conftest.py`: bare `import boto3` replaced with guarded `AWS_AVAILABLE` flag; AWS provider tests auto-skip when `[aws]` extra is absent. + +### Notes + +- `boto3` + `botocore` **remain in core** `[project.dependencies]` for now to + preserve backward compatibility — `pip install orb-py` still works unchanged. +- When the second provider (Azure / GCP / OCI) lands, AWS deps will move from + core to the `[aws]` extra and a major-version bump will signal the breaking + change. Operators who use `pip install orb-py[aws]` today will see no + behavior change at that cutover. + [Compare with latest](https://github.com/awslabs/open-resource-broker/compare/v0.1.0rc0...HEAD) ### Features diff --git a/README.md b/README.md index b36233765..40d970c16 100644 --- a/README.md +++ b/README.md @@ -83,12 +83,36 @@ Get ORB installed and configured for your environment.
Installation -### Standard install +### Standard install (core only — no provider) ```bash pip install orb-py ``` +ORB boots cleanly with no provider registered. Any command that needs a provider +will return a clear "no provider configured" error rather than an ImportError. + +### Per-provider install + +```bash +pip install "orb-py[aws]" # AWS provider (boto3 + botocore) +pip install "orb-py[aws,cli]" # AWS provider + colored CLI output +pip install "orb-py[aws,api]" # AWS provider + REST API server +pip install "orb-py[monitoring-aws]" # AWS provider + full monitoring stack +pip install "orb-py[all]" # All providers + all features +``` + +### Provider extras matrix + +| Use case | Install command | +|----------|----------------| +| Core only (no provider) | `pip install orb-py` | +| AWS operator | `pip install "orb-py[aws]"` | +| AWS + colored CLI | `pip install "orb-py[aws,cli]"` | +| AWS + REST API | `pip install "orb-py[aws,api]"` | +| AWS + monitoring | `pip install "orb-py[monitoring-aws]"` | +| Full (all providers + features) | `pip install "orb-py[all]"` | + ### With colored CLI output ```bash diff --git a/config/aws_templates.json b/config/aws_templates.json index 5b491d626..2b1b80706 100644 --- a/config/aws_templates.json +++ b/config/aws_templates.json @@ -3,7 +3,7 @@ "templates": [ { "templateId": "EC2Fleet-Instant-OnDemand", - "maxNumber": 10, + "maxNumber": 100, "fleetType": "instant", "vmTypes": { "t3.medium": 2, @@ -22,7 +22,7 @@ }, { "templateId": "EC2Fleet-Instant-Spot", - "maxNumber": 10, + "maxNumber": 100, "fleetType": "instant", "vmTypes": { "t3.medium": 2, @@ -42,7 +42,7 @@ }, { "templateId": "EC2Fleet-Instant-Mixed", - "maxNumber": 10, + "maxNumber": 100, "fleetType": "instant", "vmTypes": { "t3.medium": 2, @@ -63,7 +63,7 @@ }, { "templateId": "EC2Fleet-Request-OnDemand", - "maxNumber": 15, + "maxNumber": 100, "fleetType": "request", "vmTypes": { "t3.medium": 2, @@ -82,7 +82,7 @@ }, { "templateId": "EC2Fleet-Request-Spot", - "maxNumber": 20, + "maxNumber": 100, "fleetType": "request", "vmTypes": { "t3.medium": 2, @@ -102,7 +102,7 @@ }, { "templateId": "EC2Fleet-Request-Mixed", - "maxNumber": 25, + "maxNumber": 100, "fleetType": "request", "vmTypes": { "t3.medium": 2, @@ -125,7 +125,7 @@ }, { "templateId": "EC2Fleet-Maintain-OnDemand", - "maxNumber": 12, + "maxNumber": 100, "fleetType": "maintain", "vmTypes": { "t3.medium": 2, @@ -144,7 +144,7 @@ }, { "templateId": "EC2Fleet-Maintain-Spot", - "maxNumber": 30, + "maxNumber": 100, "fleetType": "maintain", "vmTypes": { "t3.medium": 2, @@ -164,7 +164,7 @@ }, { "templateId": "EC2Fleet-Maintain-Mixed", - "maxNumber": 50, + "maxNumber": 100, "fleetType": "maintain", "vmTypes": { "t3.medium": 2, @@ -187,7 +187,7 @@ }, { "templateId": "SpotFleet-Request-LowestPrice", - "maxNumber": 20, + "maxNumber": 100, "fleetType": "request", "vmTypes": { "t3.medium": 2, @@ -208,7 +208,7 @@ }, { "templateId": "SpotFleet-Request-Diversified", - "maxNumber": 25, + "maxNumber": 100, "fleetType": "request", "vmTypes": { "t3.medium": 2, @@ -229,7 +229,7 @@ }, { "templateId": "SpotFleet-Request-CapacityOptimized", - "maxNumber": 30, + "maxNumber": 100, "fleetType": "request", "vmTypes": { "t3.medium": 2, @@ -250,7 +250,7 @@ }, { "templateId": "SpotFleet-Maintain-LowestPrice", - "maxNumber": 15, + "maxNumber": 100, "fleetType": "maintain", "vmTypes": { "t3.medium": 2, @@ -271,7 +271,7 @@ }, { "templateId": "SpotFleet-Maintain-Diversified", - "maxNumber": 20, + "maxNumber": 100, "fleetType": "maintain", "vmTypes": { "t3.medium": 2, @@ -292,7 +292,7 @@ }, { "templateId": "SpotFleet-Maintain-CapacityOptimized", - "maxNumber": 25, + "maxNumber": 100, "fleetType": "maintain", "vmTypes": { "t3.medium": 2, @@ -313,7 +313,7 @@ }, { "templateId": "ASG-OnDemand", - "maxNumber": 15, + "maxNumber": 100, "vmTypes": { "t3.medium": 2, "t3.xlarge": 4 @@ -331,7 +331,7 @@ }, { "templateId": "ASG-Spot", - "maxNumber": 20, + "maxNumber": 100, "vmTypes": { "t3.medium": 2, "t3.xlarge": 4 @@ -350,7 +350,7 @@ }, { "templateId": "ASG-Mixed", - "maxNumber": 25, + "maxNumber": 100, "vmTypes": { "t3.medium": 2, "t3.large": 2, @@ -370,7 +370,7 @@ }, { "templateId": "RunInstances-OnDemand", - "maxNumber": 5, + "maxNumber": 100, "vmTypes": { "t3.medium": 2 }, @@ -387,7 +387,7 @@ }, { "templateId": "RunInstances-Spot", - "maxNumber": 10, + "maxNumber": 100, "vmTypes": { "t3.medium": 2 }, diff --git a/dev-tools/testing/run_tests.py b/dev-tools/testing/run_tests.py index ef9591959..f9827ee20 100755 --- a/dev-tools/testing/run_tests.py +++ b/dev-tools/testing/run_tests.py @@ -153,7 +153,7 @@ def main(): markers.append("e2e") if args.onaws: - test_paths.append("tests/onaws") + test_paths.append("tests/providers/aws/live") markers.append("aws") if args.fast: diff --git a/docs/root/architecture/clean_architecture.md b/docs/root/architecture/clean_architecture.md index f77f1916e..78659533f 100644 --- a/docs/root/architecture/clean_architecture.md +++ b/docs/root/architecture/clean_architecture.md @@ -434,4 +434,10 @@ class TemplateRepositoryImpl(TemplateRepository): Unit Tests (Domain + Application) ``` +## Adding a New Provider + +The provider layer is the primary extensibility surface for new cloud platforms. For a step-by-step guide covering the mandatory glue points and all extension point registries, see: + +- [Adding a Provider](../developer_guide/adding_a_provider.md) + This Clean Architecture implementation ensures that the Open Resource Broker maintains clear separation of concerns, high testability, and flexibility for future changes while adhering to established software engineering principles. diff --git a/docs/root/developer_guide/adding_a_provider.md b/docs/root/developer_guide/adding_a_provider.md new file mode 100644 index 000000000..ec599503f --- /dev/null +++ b/docs/root/developer_guide/adding_a_provider.md @@ -0,0 +1,584 @@ +# Adding a Provider + +This guide walks through adding a new cloud provider (such as Azure, GCP, or OCI) to the Open Resource Broker. It is aimed at developers who are already familiar with the overall architecture and want a concrete checklist of required steps and extension points. + +## Prerequisites + +Before reading this guide, review: + +- [Clean Architecture](../architecture/clean_architecture.md) — layer boundaries and dependency rules +- [Strategy Pattern](../patterns/strategy_pattern.md) — how provider strategies are structured +- [Ports and Adapters](../patterns/ports_and_adapters.md) — how registries decouple providers from shared infrastructure + +## Overview + +ORB's provider system is built around an extension-point model: all provider-specific behaviour is registered through a set of dedicated registries at startup. Shared infrastructure (the CLI, the scheduler, the REST API, the DI container) never imports provider packages directly; instead it queries registries that were populated during bootstrap. + +The goal of this model is that adding a new provider should touch exactly: + +- `src/orb/providers//` — your provider package (all provider logic lives here) +- Three glue points in shared code: an enum entry, a registration call, and a bootstrap block + +The AWS provider is the canonical reference implementation. Refer to `src/orb/providers/aws/registration.py` when you need to see a working example of every registration call described below. + +### Provider package layout + +Mirror the AWS structure: + +``` +src/orb/providers// + __init__.py + registration.py # All registration functions for this provider + strategy/ + _provider_strategy.py + cli/ + _cli_spec.py + configuration/ + config.py + template_extension.py + domain/ + template/ + _template_aggregate.py + _template_dto_config.py + scheduler/ + hostfactory_field_mapping.py + auth/ + _auth_strategy.py + defaults_loader.py +``` + +## Mandatory steps + +Complete these steps in order. Each one has a corresponding section in the extension points reference below. + +### 1. Create the provider package + +Create `src/orb/providers//` following the layout above. The strategy class must extend `BaseProviderStrategy` from `orb.providers.base.strategy.base_provider_strategy`. + +### 2. Add an enum entry + +Add your provider to `ProviderType` in `src/orb/domain/base/provider_interfaces.py`: + +```python +class ProviderType(str, Enum): + AWS = "aws" + AZURE = "azure" # new entry +``` + +Use a short, lowercase string that matches the string keys used in all registry calls. + +### 3. Add a registration call in `providers/registration.py` + +Add your provider to the two functions in `src/orb/providers/registration.py`: + +```python +def register_all_provider_cli_specs() -> None: + from orb.domain.base.ports.provider_cli_spec_port import CLISpecRegistry + from orb.providers.azure.cli.azure_cli_spec import AzureCLISpec + + if CLISpecRegistry.get("azure") is None: + CLISpecRegistry.register("azure", AzureCLISpec()) + + +def register_all_provider_types() -> None: + from orb.providers.registry import get_provider_registry + + registry = get_provider_registry() + + from orb.providers.aws.registration import register_aws_provider + register_aws_provider(registry) + + from orb.providers.azure.registration import register_azure_provider + register_azure_provider(registry) +``` + +### 4. Add a bootstrap block in `bootstrap/provider_services.py` + +Add a `find_spec`-guarded block to `_register_provider_utility_services` in `src/orb/bootstrap/provider_services.py`. Use the same pattern as the AWS block immediately above it: + +```python +if importlib.util.find_spec("orb.providers.azure"): + try: + from orb.providers.azure.registration import register_azure_services_with_di + register_azure_services_with_di(container) + except Exception as e: + logger.warning("Failed to register Azure utility services: %s", str(e)) +``` + +The `find_spec` guard means the rest of ORB still runs when the provider package is not installed (useful for minimal deployments and test environments that exclude a specific provider's SDK). + +### 5. Add SDK dependencies + +Add your cloud provider SDK to `pyproject.toml` and regenerate the lockfile: + +```toml +[project.optional-dependencies] +azure = ["azure-mgmt-compute>=30.0", "azure-identity>=1.15"] +``` + +Then run `uv lock` to update `uv.lock`. + +--- + +## Extension points reference + +Each registry is a class-level singleton. Register during startup; never register lazily inside a request handler. + +### ProviderRegistry + +**Location:** `src/orb/providers/registry/provider_registry.py` + +**What it does:** The central strategy factory. When a request arrives for a named provider, `ProviderRegistry` calls your `strategy_factory` to create the strategy instance and `config_factory` to parse configuration data. + +**When to register:** In your `register__provider(registry)` function called from `providers/registration.py`. + +**How to register:** + +```python +# src/orb/providers/azure/registration.py + +def register_azure_provider(registry=None) -> None: + from orb.providers.registry import get_provider_registry + from orb.providers.azure.strategy.azure_provider_strategy import AzureProviderStrategy + + if registry is None: + registry = get_provider_registry() + + registry.register_provider( + provider_type="azure", + strategy_factory=create_azure_strategy, + config_factory=create_azure_config, + resolver_factory=create_azure_resolver, # return None if not needed + validator_factory=create_azure_validator, # return None if not needed + strategy_class=AzureProviderStrategy, + default_api=_load_azure_default_api(), # reads from azure_defaults.json + ) +``` + +**AWS reference:** `register_aws_provider` in `src/orb/providers/aws/registration.py`. + +--- + +### CLISpecRegistry + ProviderCLISpecPort + +**Location:** `src/orb/domain/base/ports/provider_cli_spec_port.py` + +**What it does:** Supplies provider-specific CLI argument definitions, input validation logic, field extraction from parsed arguments, and display formatting. The shared `cli/args.py` and `interface/init_command_handler.py` iterate over `CLISpecRegistry.all()` rather than hard-coding provider flags. + +**When to register:** In `register_all_provider_cli_specs()` in `providers/registration.py`. This function is called before application context exists, so keep it lightweight — no network calls, no DI container access. + +**How to register:** + +```python +from orb.domain.base.ports.provider_cli_spec_port import CLISpecRegistry +from orb.providers.azure.cli.azure_cli_spec import AzureCLISpec + +CLISpecRegistry.register("azure", AzureCLISpec()) +``` + +Implement `ProviderCLISpecPort` on your spec class: + +```python +class AzureCLISpec: + def add_arguments(self, parser) -> None: + parser.add_argument("--azure-subscription-id", help="Azure subscription ID") + parser.add_argument("--azure-resource-group", help="Azure resource group") + + def validate(self, args) -> list[str]: + errors = [] + if not getattr(args, "azure_subscription_id", None): + errors.append("--azure-subscription-id is required for Azure providers") + return errors + + def extract_fields(self, args) -> dict: + return { + "subscription_id": args.azure_subscription_id, + "resource_group": getattr(args, "azure_resource_group", None), + } +``` + +**AWS reference:** `src/orb/providers/aws/cli/aws_cli_spec.py`. + +--- + +### TemplateExtensionRegistry + +**Location:** `src/orb/domain/template/extensions.py` + +**What it does:** Holds a typed Pydantic model class for each provider's template configuration. When `TemplateDTO.from_domain` serialises a template, it calls `TemplateExtensionRegistry.get_extension_class(provider_type)` to obtain and populate the `provider_config` field. This replaces ad-hoc `metadata` dict keys and `getattr(template, "validate_")` dynamic dispatch. + +**When to register:** In `register__extensions()`, called from `initialize__provider()` during startup. + +**How to register:** + +```python +from orb.domain.template.extensions import TemplateExtensionRegistry +from orb.providers.azure.configuration.template_extension import AzureTemplateExtensionConfig + +TemplateExtensionRegistry.register_extension("azure", AzureTemplateExtensionConfig) +``` + +`AzureTemplateExtensionConfig` should be a Pydantic `BaseModel` with `get_provider_type()` returning `"azure"` and a `to_template_defaults()` method returning a `dict` of default values: + +```python +from pydantic import BaseModel, Field + +class AzureTemplateExtensionConfig(BaseModel): + vm_size: str = Field("Standard_D2s_v3", description="Azure VM size") + location: str = Field("eastus", description="Azure region") + resource_group: str = Field("", description="Azure resource group") + + def get_provider_type(self) -> str: + return "azure" + + def to_template_defaults(self) -> dict: + return self.model_dump() +``` + +**AWS reference:** `src/orb/providers/aws/configuration/template_extension.py` and the `register_aws_extensions` function in `src/orb/providers/aws/registration.py`. + +--- + +### AuthRegistry + +**Location:** `src/orb/infrastructure/auth/registry.py` + +**What it does:** Maps auth strategy names (strings like `"iam"`, `"cognito"`) to auth strategy classes. The REST server calls `AuthRegistry.get_strategy(name, **config)` rather than dispatching through `if/elif` chains. Register all auth strategies your provider supports. + +**When to register:** In `register__auth_strategies()`, called from `initialize__provider()` during startup. + +**How to register:** + +```python +from orb.infrastructure.auth.registry import get_auth_registry + +registry = get_auth_registry() + +if not registry.is_registered("azure_ad"): + from orb.providers.azure.auth.azure_ad_strategy import AzureADAuthStrategy + registry.register_strategy("azure_ad", AzureADAuthStrategy) + +if not registry.is_registered("managed_identity"): + from orb.providers.azure.auth.managed_identity_strategy import ManagedIdentityAuthStrategy + registry.register_strategy("managed_identity", ManagedIdentityAuthStrategy) +``` + +**AWS reference:** `register_aws_auth_strategies` in `src/orb/providers/aws/registration.py`. + +--- + +### FieldMappingRegistry + +**Location:** `src/orb/infrastructure/scheduler/hostfactory/field_mapping_registry.py` + +**What it does:** Holds a per-provider `FieldMappingPort` adapter. The HostFactory scheduler calls `FieldMappingRegistry.get(provider_type)` to translate IBM Spectrum Symphony camelCase field names to the provider's snake_case equivalents, apply provider-specific defaults, and resolve CPU/RAM values from a provider-specific instance type catalogue. + +**When to register:** In `initialize__provider()`, after other registrations. + +**How to register:** + +```python +from orb.infrastructure.scheduler.hostfactory.field_mapping_registry import FieldMappingRegistry +from orb.providers.azure.scheduler.hostfactory_field_mapping import AzureFieldMapping + +FieldMappingRegistry.register("azure", AzureFieldMapping()) +``` + +Implement `FieldMappingPort` in your mapping class. The two critical methods are `map_fields(raw: dict) -> dict` (camelCase-to-snake_case translation + provider defaults) and `resolve_cpu_ram(vm_size: str) -> tuple[int, int]` (returns `(cpu_count, ram_mb)` from your provider's instance catalogue). + +**AWS reference:** `src/orb/providers/aws/scheduler/hostfactory_field_mapping.py` and `register_aws_provider` in `src/orb/providers/aws/registration.py` (the `FieldMappingRegistry.register` call near the end of `initialize_aws_provider`). + +--- + +### DefaultsLoaderRegistry + +**Location:** `src/orb/providers/registry/defaults_loader_registry.py` + +**What it does:** Holds a per-provider `ProviderDefaultsLoaderPort` that loads a provider's defaults JSON file. The template defaults service calls this registry to populate provider-specific default values rather than hard-coding file paths for each provider. + +**When to register:** In `initialize__provider()`. + +**How to register:** + +```python +from orb.providers.registry.defaults_loader_registry import DefaultsLoaderRegistry +from orb.providers.azure.defaults_loader import AzureDefaultsLoader + +DefaultsLoaderRegistry.register("azure", AzureDefaultsLoader()) +``` + +`AzureDefaultsLoader` implements `ProviderDefaultsLoaderPort` and typically reads from a JSON file bundled alongside your provider package (e.g. `src/orb/providers/azure/config/azure_defaults.json`). + +**AWS reference:** `src/orb/providers/aws/defaults_loader.py` and the `DefaultsLoaderRegistry.register` call in `initialize_aws_provider`. + +--- + +### TemplateAdapterPort + +**Location:** `src/orb/domain/base/ports/template_adapter_port.py` + +**What it does:** Resolves provider-specific template fields that require a live API call (for example, looking up an AMI by name on AWS, or resolving an image reference on Azure). Registered in the DI container as a singleton, not in a class-level registry. + +**When to register:** In `register__services_with_di(container)`, called from the `find_spec`-guarded block in `bootstrap/provider_services.py`. + +**How to register:** + +```python +from orb.domain.base.ports.template_adapter_port import TemplateAdapterPort + +def create_azure_template_adapter(c): + from orb.providers.azure.infrastructure.adapters.template_adapter import AzureTemplateAdapter + from orb.domain.base.ports import LoggingPort, ConfigurationPort + return AzureTemplateAdapter( + logger=c.get(LoggingPort), + config=c.get(ConfigurationPort), + ) + +container.register_singleton(TemplateAdapterPort, create_azure_template_adapter) +``` + +**AWS reference:** `register_aws_services_with_di` in `src/orb/providers/aws/registration.py`. + +--- + +### TemplateExampleGeneratorPort + +**Location:** `src/orb/domain/base/ports/template_example_generator_port.py` + +**What it does:** Generates example template JSON for the `orb template generate` command. ORB resolves this port from the DI container and calls it to produce provider-appropriate example output. No live API connection is required; the generator uses handler class metadata only. + +**When to register:** In `register__services_with_di(container)`, alongside the template adapter. + +**How to register:** + +```python +from orb.domain.base.ports.template_example_generator_port import TemplateExampleGeneratorPort + +def create_azure_example_generator(c): + from orb.providers.azure.adapters.template_example_generator_adapter import ( + AzureTemplateExampleGeneratorAdapter, + ) + return AzureTemplateExampleGeneratorAdapter() + +container.register_singleton(TemplateExampleGeneratorPort, create_azure_example_generator) +``` + +**AWS reference:** The `TemplateExampleGeneratorPort` block inside `register_aws_services_with_di`. + +--- + +## OperationOutcome contract + +Every strategy method that performs a cloud operation returns `OperationOutcome`, a discriminated union defined in `src/orb/domain/base/operation_outcome.py`: + +```python +OperationOutcome = Accepted | Completed | RequiresFollowUp | Failed +``` + +Choose the correct variant based on what the cloud API actually tells you: + +| Variant | Use when | +|---|---| +| `Accepted` | The provider acknowledged the request but resources are not yet in their final state. Include a provider-side tracking ID in `request_id` and the in-flight resource IDs in `pending_resource_ids`. The orchestration layer will poll `get_status` until a terminal outcome is returned. | +| `Completed` | All resources have reached their terminal state in this call. Include the final resource IDs in `resource_ids`. | +| `RequiresFollowUp` | The provider acknowledged the request but a domain-level follow-up action is needed beyond simple polling (for example, a webhook registration or a secondary API call). Populate a `FollowUpContext` describing what to do next. | +| `Failed` | The operation failed. Set `recoverable=True` for transient failures (throttles, temporary capacity shortages) and `False` for hard failures (invalid configuration, permission denied). | + +**AWS example — `acquire` always returns `Accepted`:** + +```python +async def acquire(self, request: Request) -> OperationOutcome: + result = await self.execute_operation(operation) + if not result.success: + return Failed(error=result.error_message or "acquire failed", recoverable=False) + return Accepted( + request_id=str(request.request_id), + pending_resource_ids=result.data.get("resource_ids", []), + ) +``` + +EC2 Fleet, SpotFleet, and RunInstances all accept the request immediately and let instances transition through `pending → running` asynchronously. The correct outcome is always `Accepted`. + +**Azure ARM example — `return_machines` with multi-step async teardown:** + +Azure resource deletion may trigger a long-running ARM operation that requires a separate status poll URL: + +```python +async def return_machines( + self, machine_ids: list[str], request: Request +) -> OperationOutcome: + response = await self._arm_client.begin_delete(resource_ids=machine_ids) + if response.needs_follow_up: + return RequiresFollowUp( + context=AzureArmFollowUpContext( + operation_url=response.poll_url, + resource_ids=machine_ids, + follow_up_kind="arm_async_delete", + ) + ) + if response.done: + return Completed(resource_ids=machine_ids) + return Accepted( + request_id=response.operation_id, + pending_resource_ids=machine_ids, + ) +``` + +Always dispatch on `OperationOutcome` exhaustively using `match` + `assert_never` in calling code so pyright catches any future variant additions at compile time. + +--- + +## Anti-patterns + +The following patterns must not appear in new provider code. Each one creates a coupling that prevents new providers from being added without editing shared infrastructure. + +### Do not edit `cli/args.py` for provider-specific flags + +`cli/args.py` iterates `CLISpecRegistry.all()`. Adding flags for a specific provider here leaks provider knowledge into shared code and means all users see flags that may not apply to their provider. + +```python +# Wrong — in src/orb/cli/args.py +parser.add_argument("--azure-subscription-id", ...) + +# Right — in src/orb/providers/azure/cli/azure_cli_spec.py +class AzureCLISpec: + def add_arguments(self, parser) -> None: + parser.add_argument("--azure-subscription-id", ...) +``` + +### Do not add `if provider_type == "x"` branches in shared services + +Branching on provider type in shared services (template defaults service, provisioning orchestration service, scheduler) means every new provider requires editing code it should not know about. + +```python +# Wrong — in any shared service +if provider_type == "azure": + apply_azure_defaults(template) +elif provider_type == "aws": + apply_aws_defaults(template) + +# Right — register a DefaultsLoader and let the registry dispatch +loader = DefaultsLoaderRegistry.get(provider_type) +if loader: + defaults = loader.load_defaults() +``` + +### Do not use `getattr(template, f"validate_{provider_type}")` dynamic dispatch + +String-keyed `getattr` dispatch is invisible to the type checker. Renames silently break at runtime and there is no way to enumerate valid provider types statically. + +```python +# Wrong +if hasattr(template, f"validate_{provider_type}"): + getattr(template, f"validate_{provider_type}")() + +# Right — use TemplateExtensionRegistry for unconditional dispatch +extension_class = TemplateExtensionRegistry.get_extension_class(provider_type) +if extension_class: + extension_class.model_validate(template.provider_config or {}) +``` + +### Do not add provider-specific fields to the shared `TemplateAggregate` + +The domain template aggregate is provider-agnostic. Adding an `azure_resource_group` field to `domain/template/template_aggregate.py` forces all providers to handle a field they do not own and breaks the provider isolation guarantee. + +```python +# Wrong — in src/orb/domain/template/template_aggregate.py +azure_resource_group: str | None = None + +# Right — in AzureTemplateExtensionConfig (a Pydantic model inside the Azure package) +class AzureTemplateExtensionConfig(BaseModel): + resource_group: str = "" +``` + +### Do not add provider-specific fields to the shared `TemplateDTO` + +`TemplateDTO` is the serialisation boundary between the application and API layers. Provider-specific fields belong in the `provider_config: BaseModel | None` extension field populated by `TemplateExtensionRegistry`, not as top-level DTO attributes. + +```python +# Wrong — in src/orb/application/dto/template_dto.py +azure_vm_size: str | None = None + +# Right — TemplateDTO.provider_config carries AzureTemplateExtensionConfig +# automatically when the extension is registered +``` + +### Do not add provider strings to domain value objects + +`domain/base/value_objects.py` and similar domain files must not contain string literals for specific providers. Use `ProviderType` where a typed enum is appropriate, and registries for everything else. + +```python +# Wrong — in src/orb/domain/base/value_objects.py +KNOWN_PROVIDERS = ["aws", "azure", "gcp"] + +# Right — ProviderRegistry.registered_providers() returns this list dynamically +``` + +### Do not add provider-specific imports to shared infrastructure + +Shared infrastructure files (`infrastructure/scheduler/`, `api/server.py`, etc.) must not import from `providers//`. Doing so creates a hard dependency that prevents the package from being imported in environments where that provider's SDK is not installed. + +```python +# Wrong — in src/orb/infrastructure/scheduler/hostfactory/hostfactory_strategy.py +from orb.providers.aws.utilities.ec2.instances import derive_cpu_ram_from_instance_type + +# Right — FieldMappingRegistry.get(provider_type).resolve_cpu_ram(vm_size) +``` + +--- + +## Test layout + +Mirror the source layout under `tests/providers//`: + +``` +tests/providers// + conftest.py # shared fixtures for this provider + unit/ # pure unit tests, no cloud calls, no mocks of cloud SDK + test__strategy.py + test__cli_spec.py + test__template_extension.py + test__field_mapping.py + moto/ # mocked integration tests (use a mock SDK equivalent) + conftest.py + test__acquire.py + test__return.py + live/ # real-cloud tests, gated by --live flag + conftest.py # skips all tests unless --live is passed + test__connectivity.py + test__roundtrip.py + contract/ # contract tests verifying OperationOutcome variants + test_outcome_variants.py +``` + +Use the per-provider `conftest.py` to define fixtures that supply mock clients, provider configs, and sample request/template domain objects. Keep `moto/` and `live/` sub-packages separate so CI can include mocked tests and exclude live tests without test selection gymnastics. + +Gate live tests with a custom pytest marker: + +```python +# tests/providers//live/conftest.py +import pytest + +def pytest_collection_modifyitems(config, items): + if not config.getoption("--live", default=False): + skip = pytest.mark.skip(reason="pass --live to run real-cloud tests") + for item in items: + if "live" in str(item.fspath): + item.add_marker(skip) +``` + +The AWS provider tests are the reference layout: + +- Unit tests: `tests/providers/aws/unit/` +- Mocked integration tests: `tests/providers/aws/moto/` +- Real-AWS tests: `tests/providers/aws/live/` +- Contract tests: `tests/providers/aws/contract/` + +--- + +## Cross-references + +- [Clean Architecture](../architecture/clean_architecture.md) — layer boundaries enforced by architecture tests +- [Strategy Pattern](../patterns/strategy_pattern.md) — how `BaseProviderStrategy` and `ProviderRegistry` work together +- [Ports and Adapters](../patterns/ports_and_adapters.md) — the port/registry decoupling pattern used throughout +- AWS reference implementation: `src/orb/providers/aws/registration.py` diff --git a/makefiles/common.mk b/makefiles/common.mk index 9a1f2e8cb..87cd6709d 100644 --- a/makefiles/common.mk +++ b/makefiles/common.mk @@ -42,7 +42,7 @@ TESTS := tests TESTS_UNIT := $(TESTS)/unit TESTS_INTEGRATION := $(TESTS)/integration TESTS_E2E := $(TESTS)/e2e -TESTS_ONMOTO := $(TESTS)/onmoto +TESTS_ONMOTO := $(TESTS)/providers/aws/moto TESTS_PERFORMANCE := $(TESTS)/performance TESTS_INFRASTRUCTURE := $(TESTS)/infrastructure TESTS_PROVIDERS := $(TESTS)/providers diff --git a/makefiles/deploy.mk b/makefiles/deploy.mk index 3146447ce..2f8910899 100644 --- a/makefiles/deploy.mk +++ b/makefiles/deploy.mk @@ -20,14 +20,6 @@ container-show-version: dev-install ## Show container version info container-run: dev-install ## Run container build @./dev-tools/container/container_dispatcher.py run -container-health-check: ## Poll container health check endpoint - @./dev-tools/container/container_health_check.py - -test-docker: ## Run Docker test suite (build, startup, security, pytest tests/docker/) - @./dev-tools/testing/test-docker.sh - -# Dummy targets removed (consolidated in quality.mk) - # @SECTION Documentation # Documentation targets docs: dev-install ## Build/manage docs (usage: make docs [serve] [deploy] [version=X.X.X] [list] [delete=X.X.X] [clean]) diff --git a/makefiles/dev.mk b/makefiles/dev.mk index 114078eab..83e09198f 100644 --- a/makefiles/dev.mk +++ b/makefiles/dev.mk @@ -53,8 +53,8 @@ test: dev-install ## Run tests (usage: make test [unit|integration|e2e|coverage test-report: dev-install ## Generate comprehensive test report (PRESERVE: used by ci.yml) ./dev-tools/testing/run_tests.py --all --coverage --junit-xml=test-results-combined.xml --cov-xml=coverage-combined.xml --html-coverage --maxfail=1 --timeout=60 -system-tests: dev-install ## Run system integration tests - @uv run python -m pytest tests/onaws/test_onaws.py -v -m manual_aws --no-cov --tb=long +system-tests: dev-install ## Run system integration tests (real AWS) + @uv run python -m pytest tests/providers/aws/live/test_onaws.py -v -m manual_aws --no-cov --tb=long # Backward compatibility aliases (direct calls to avoid loops) test-unit: dev-install @@ -93,6 +93,36 @@ test-html: dev-install test-docker: dev-install ## Run Docker containerization tests @./dev-tools/testing/test-docker.sh +# Provider-aware test targets +# Parallel worker count for pytest. Capped at half the available CPU cores so +# parallel test runs (especially live-AWS, which spawn ORB server subprocesses, +# boto3 sessions, and long polls per worker) don't exhaust system memory or +# saturate the network. Override per-invocation: make test-providers-aws-live PYTEST_WORKERS=4 +# Use $(shell ...) so the math runs once when make starts, not on every recipe. +_HALF_CPU := $(shell python3 -c 'import os; print(max(1, (os.cpu_count() or 2) // 2))') +PYTEST_WORKERS ?= $(_HALF_CPU) + +test-no-live: dev-install ## Run all tests except live cloud suites (pre-PR check) + @uv run pytest --no-cov -q -ra -n $(PYTEST_WORKERS) --ignore=tests/providers/aws/live + +test-providers: dev-install ## Run all provider tests except live + @uv run pytest --no-cov -q -ra -n $(PYTEST_WORKERS) tests/providers --ignore=tests/providers/aws/live + +test-providers-aws: dev-install ## Run AWS provider unit + moto tests + @uv run pytest --no-cov -q -ra -n $(PYTEST_WORKERS) tests/providers/aws/unit tests/providers/aws/moto + +test-providers-aws-unit: dev-install ## Run AWS provider unit tests only + @uv run pytest --no-cov -q -ra -n $(PYTEST_WORKERS) tests/providers/aws/unit + +test-providers-aws-moto: dev-install ## Run AWS moto (mocked) tests only + @uv run pytest --no-cov -q -ra -n $(PYTEST_WORKERS) tests/providers/aws/moto + +test-providers-aws-live: dev-install ## Run AWS live tests (requires real AWS credentials; parallel by default) + @uv run --extra api pytest --no-cov -q -ra -n $(PYTEST_WORKERS) --live tests/providers/aws/live + +test-architecture: dev-install ## Run architecture compliance tests + @uv run pytest --no-cov -q -ra -n $(PYTEST_WORKERS) tests/unit/architecture tests/unit/test_architectural_compliance.py + # Dummy targets removed (consolidated in quality.mk) # @SECTION Development Tools diff --git a/pyproject.toml b/pyproject.toml index 32fed287c..3892049ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,7 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ - # Core dependencies (always needed) - "boto3>=1.42.21", - "botocore>=1.42.21", + # Core dependencies (always needed, never provider-specific) "pydantic>=2.0.0", "pydantic-settings>=2.0.0", "sqlalchemy>=2.0.0", @@ -55,9 +53,43 @@ dependencies = [ "requests>=2.31.0", "PyYAML>=6.0.0", # Exported API (lazy-loaded) "jsonschema>=4.17.0", + # AWS provider deps — currently in core for backward compatibility. + # The canonical install path going forward is `pip install orb-py[aws]`, + # which already works today. When the second provider lands these will + # move to the [aws] extra and a major-version bump will signal the + # breaking change for operators who installed without extras. + "boto3>=1.42.21", + "botocore>=1.42.21", ] [project.optional-dependencies] +# ── Provider extras ──────────────────────────────────────────────────────── +# AWS provider — explicit opt-in alias for the AWS deps currently in core. +# Pinning the same versions keeps `pip install orb-py[aws]` resolving identically +# to the bare install and future-proofs operator install commands for the day +# AWS deps move out of core. +aws = [ + "boto3>=1.42.21", + "botocore>=1.42.21", +] + +# Placeholder entries for future providers (add when provider is implemented) +# azure = [ +# "azure-mgmt-compute>=30.0.0", +# "azure-identity>=1.15.0", +# "azure-mgmt-network>=25.0.0", +# ] +# gcp = [ +# "google-cloud-compute>=1.14.0", +# "google-auth>=2.23.0", +# ] + +# Meta-extra: all currently implemented providers +all-providers = [ + "orb-py[aws]", +] + +# ── Feature extras ───────────────────────────────────────────────────────── # CLI enhancements (colored output) cli = [ "rich>=13.3.0", @@ -72,20 +104,34 @@ api = [ "jinja2>=3.1.0", ] -# Monitoring & observability (for API/production deployments) +# Monitoring & observability (provider-neutral — no boto3 instrumentation here) monitoring = [ "opentelemetry-api>=1.20.0", "opentelemetry-sdk>=1.20.0", "opentelemetry-instrumentation-fastapi>=0.41b0", - "opentelemetry-instrumentation-boto>=0.41b0", "opentelemetry-instrumentation-sqlalchemy>=0.41b0", "prometheus-client>=0.17.0", "psutil>=5.9.0", ] -# All optional features +# AWS-specific monitoring (boto3 instrumentation; requires [aws]) +monitoring-aws = [ + "orb-py[aws,monitoring]", + "opentelemetry-instrumentation-boto>=0.41b0", +] + +# All optional features (providers + features) all = [ - "orb-py[cli,api,monitoring]", + "orb-py[cli,api,monitoring,all-providers]", +] + +# ── Test extras ──────────────────────────────────────────────────────────── +# AWS test dependencies — moto + response mocking +test-aws = [ + "orb-py[aws]", + "moto[ec2,iam,ec2instanceconnect,autoscaling,ssm,dynamodb,sts,sqs,sns]>=5.1.19,<6.0.0", + "responses>=0.24.0,<1.0.0", + "requests-mock>=1.11.0,<2.0.0", ] # --------------------------------------------------------------------------- @@ -151,7 +197,7 @@ ci = [ "wheel>=0.41.3,<1.0.0", ] dev = [ - "orb-py[ci]", + "orb-py[ci,all-providers]", "virtualenv>=20.26.6", "pre-commit>=3.5.0,<5.0.0", "bump2version>=1.0.1,<2.0.0", @@ -227,6 +273,8 @@ ci = [ "peewee>=3.18.3", # Security override for safety's vulnerable marshmallow dependency (CVE-2025-68480) "marshmallow>=4.1.2", + # Security override for lxml (CVE-2026-41066) — transitive via cyclonedx-bom + pip-audit + "lxml>=6.1.0", # Documentation "mkdocs>=1.5.0,<2.0.0", @@ -291,7 +339,7 @@ include-package-data = true [tool.pytest.ini_options] testpaths = ["tests"] -norecursedirs = ["tests/onaws"] +norecursedirs = ["tests/providers/aws/live"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] @@ -323,6 +371,7 @@ markers = [ "simulator_limitation: Assertion weakened due to known simulator gap", "requires_real_provider: Skipped unless --run-real-provider flag passed", "moto: Tests using moto-mocked AWS (no real credentials required)", + "live: Tests requiring real AWS credentials (enabled with --live or --run-aws)", "sdk: Tests exercising the ORBClient SDK interface", "cli: marks CLI integration tests", "benchmark: marks benchmark/performance tests", @@ -373,7 +422,7 @@ title = "Open Resource Broker Coverage Report" output = "coverage.xml" [tool.ruff] -target-version = "py39" +target-version = "py310" line-length = 100 exclude = [ ".eggs", diff --git a/src/orb/api/routers/templates.py b/src/orb/api/routers/templates.py index 1e92c0c0d..8580209eb 100644 --- a/src/orb/api/routers/templates.py +++ b/src/orb/api/routers/templates.py @@ -56,7 +56,7 @@ class TemplateCreateRequest(APIRequest): template_id: str name: Optional[str] = None description: Optional[str] = None - provider_api: Optional[str] = "aws" + provider_api: Optional[str] = None image_id: Optional[str] = None instance_type: Optional[str] = None key_name: Optional[str] = None @@ -223,7 +223,7 @@ async def create_template( template_id=template_dict["template_id"], name=template_dict.get("name"), description=template_dict.get("description"), - provider_api=template_dict.get("provider_api") or "aws", + provider_api=template_dict.get("provider_api"), instance_type=template_dict.get("instance_type"), image_id=template_dict.get("image_id") or "", tags=template_dict.get("tags") or {}, diff --git a/src/orb/api/server.py b/src/orb/api/server.py index 7e5581f65..6a73859cc 100644 --- a/src/orb/api/server.py +++ b/src/orb/api/server.py @@ -230,83 +230,28 @@ def _create_auth_strategy(auth_config: Any) -> Any: """ Create authentication strategy based on configuration. + Delegates config extraction entirely to each strategy's ``from_auth_config`` + classmethod via the auth registry. No per-strategy dispatch lives here. + Args: - auth_config: Authentication configuration + auth_config: AuthConfig instance Returns: - Authentication strategy instance or None + Authentication strategy instance, or None if the strategy name is unknown + + Raises: + ConfigurationError: If the strategy is known but its config is invalid """ logger = get_logger(__name__) strategy_name = getattr(auth_config, "strategy", "unknown") try: auth_registry = get_auth_registry() + return auth_registry.get_strategy(strategy_name, auth_config) - if strategy_name == "none": - return auth_registry.get_strategy("none", enabled=False) - - elif strategy_name == "bearer_token": - bearer_config = auth_config.bearer_token or {} - secret_key = bearer_config.get("secret_key") - if not secret_key: - raise ConfigurationError( - "Bearer token authentication requires a secret_key in auth.bearer_token config. " - "Set HF_AUTH_BEARER_SECRET_KEY or configure auth.bearer_token.secret_key." - ) - if len(secret_key.encode()) < 32: - raise ConfigurationError( - "Bearer token secret_key must be at least 32 bytes for security." - ) - return auth_registry.get_strategy( - "bearer_token", - secret_key=secret_key, - algorithm=bearer_config.get("algorithm", "HS256"), - token_expiry=bearer_config.get("token_expiry", 3600), - enabled=True, - ) - - elif strategy_name == "iam": - iam_config = (auth_config.provider_auth or {}).get("iam") or {} - # Register AWS IAM strategy if not already registered - try: - from orb.providers.aws.auth.iam_strategy import IAMAuthStrategy - - auth_registry.register_strategy("iam", IAMAuthStrategy) - except ImportError: - logger.warning("AWS IAM strategy not available", exc_info=True) - return None - - return auth_registry.get_strategy( - "iam", - region=iam_config.get("region", "us-east-1"), - profile=iam_config.get("profile"), - required_actions=iam_config.get("required_actions", []), - enabled=True, - ) - - elif strategy_name == "cognito": - cognito_config = (auth_config.provider_auth or {}).get("cognito") or {} - # Register AWS Cognito strategy if not already registered - try: - from orb.providers.aws.auth.cognito_strategy import CognitoAuthStrategy - - auth_registry.register_strategy("cognito", CognitoAuthStrategy) - except ImportError: - logger.warning("AWS Cognito strategy not available", exc_info=True) - return None - - return auth_registry.get_strategy( - "cognito", - user_pool_id=cognito_config.get("user_pool_id", ""), - client_id=cognito_config.get("client_id", ""), - region=cognito_config.get("region", "us-east-1"), - enabled=True, - ) - - else: - logger.error("Unknown authentication strategy: %s", strategy_name) - return None - + except ValueError: + logger.error("Unknown authentication strategy: %s", strategy_name) + return None except Exception as e: raise ConfigurationError(f"Failed to create auth strategy '{strategy_name}': {e}") from e diff --git a/src/orb/application/commands/request_creation_handlers.py b/src/orb/application/commands/request_creation_handlers.py index 71b0607c7..572381038 100644 --- a/src/orb/application/commands/request_creation_handlers.py +++ b/src/orb/application/commands/request_creation_handlers.py @@ -11,6 +11,7 @@ CreateReturnRequestCommand, ) from orb.application.ports.query_bus_port import QueryBusPort +from orb.application.services.provider_validation_service import ProviderValidationService from orb.application.services.provisioning_orchestration_service import ( ProvisioningOrchestrationService, ) @@ -24,7 +25,9 @@ LoggingPort, ProviderSelectionPort, ) +from orb.domain.request.aggregate import Request from orb.domain.request.request_identifiers import RequestId +from orb.domain.request.request_types import RequestStatus from orb.domain.request.value_objects import RequestType @@ -46,6 +49,7 @@ def __init__( query_bus: QueryBusPort, # QueryBus is required for template lookup provider_selection_port: ProviderSelectionPort, provisioning_service: ProvisioningOrchestrationService, + provider_validation_service: ProviderValidationService, ) -> None: """Initialize the instance.""" super().__init__(logger, event_publisher, error_handler) @@ -55,7 +59,6 @@ def __init__( self._provider_selection_port = provider_selection_port # Initialize services - from orb.application.services.provider_validation_service import ProviderValidationService from orb.application.services.request_creation_service import RequestCreationService from orb.application.services.request_status_management_service import ( RequestStatusManagementService, @@ -64,9 +67,7 @@ def __init__( self._request_creation_service = RequestCreationService(logger) self._provisioning_service = provisioning_service self._status_service = RequestStatusManagementService(uow_factory, logger) - self._provider_validation_service = ProviderValidationService( - container, logger, provider_selection_port - ) + self._provider_validation_service = provider_validation_service async def validate_command(self, command: CreateRequestCommand) -> None: """Validate create request command.""" @@ -136,8 +137,6 @@ async def _load_template(self, template_id: str) -> Any: def _handle_dry_run(self, request: Any) -> Any: """Handle dry-run request.""" - from orb.domain.request.value_objects import RequestStatus - self.logger.info( "Skipping actual provisioning for request %s (dry-run mode)", request.request_id, @@ -256,8 +255,6 @@ async def execute_command(self, command: CreateReturnRequestCommand) -> None: self.logger.info("Creating return request for machines: %s", command.machine_ids) try: - from orb.domain.request.aggregate import Request - domain_config = self._container.get(DomainConfigurationService) prefix = domain_config.get_return_request_prefix() force_return = command.force_return or False @@ -424,7 +421,6 @@ async def _update_request_to_in_progress(self, request: Any) -> None: try: from orb.application.dto.commands import UpdateRequestStatusCommand from orb.application.ports.command_bus_port import CommandBusPort - from orb.domain.request.request_types import RequestStatus update_command = UpdateRequestStatusCommand( request_id=str(request.request_id), @@ -468,7 +464,6 @@ async def _execute_deprovisioning_for_request( if skipped_ids: from orb.application.dto.commands import UpdateRequestStatusCommand from orb.application.ports.command_bus_port import CommandBusPort - from orb.domain.request.request_types import RequestStatus skipped_str = ", ".join(skipped_ids) update_command = UpdateRequestStatusCommand( @@ -489,8 +484,8 @@ async def _execute_deprovisioning_for_request( skipped_str, ) else: - await self._update_request_to_completed(request) - self.logger.info("Termination initiated for request %s", request.request_id) + await self._update_request_to_terminating(request) + self.logger.info("Termination accepted for request %s", request.request_id) else: await self._update_request_to_failed(request, provisioning_result.get("errors", [])) @@ -517,75 +512,88 @@ def _update_machines_to_pending(self, machine_ids: list[str]) -> None: ) uow.machines.save(updated_machine) - async def _update_request_to_failed(self, request: Any, errors: list[str]) -> None: - """Update request status to failed.""" - try: - from orb.application.dto.commands import UpdateRequestStatusCommand - from orb.application.ports.command_bus_port import CommandBusPort - from orb.domain.request.request_types import RequestStatus - - error_message = "; ".join(errors) if errors else "Deprovisioning failed" + async def _update_request_status( + self, request: Request, status: RequestStatus, message: str + ) -> None: + """Update request status via the command bus with a UoW fallback for FAILED. - update_command = UpdateRequestStatusCommand( - request_id=str(request.request_id), - status=RequestStatus.FAILED, - message=f"Return request failed: {error_message}", - ) + On command-bus failure the error is logged. For FAILED status a direct + UoW write is attempted so the request always reaches a terminal state and + callers do not poll forever. For all other statuses the caller decides + how to handle the failure (e.g. _update_request_to_terminating re-raises + so the deprovisioning loop can apply its own fallback). + """ + from orb.application.dto.commands import UpdateRequestStatusCommand + from orb.application.ports.command_bus_port import CommandBusPort + update_command = UpdateRequestStatusCommand( + request_id=str(request.request_id), + status=status, + message=message, + ) + try: command_bus = self._container.get(CommandBusPort) await command_bus.execute(update_command) - - self.logger.info("Updated request %s status to failed", request.request_id) - + self.logger.info("Updated request %s status to %s", request.request_id, status) except Exception as update_error: self.logger.error( - "Failed to update request status: %s", + "Failed to update request %s status to %s: %s", + request.request_id, + status, update_error, exc_info=True, ) - # Force-write terminal status directly to prevent permanent stuck state - try: - from orb.domain.request.request_types import RequestStatus + if status == RequestStatus.FAILED: + # Force-write terminal status directly to prevent permanent stuck state + try: + with self.uow_factory.create_unit_of_work() as uow: + stuck_request = uow.requests.get_by_id(request.request_id) + if stuck_request: + stuck_request = stuck_request.update_status( + RequestStatus.FAILED, + "System error: failed to update status after double failure", + force=True, + ) + uow.requests.save(stuck_request) + except Exception as final_error: + self.logger.critical( + "CRITICAL: Failed to mark request %s as failed after double failure. " + "Request is stuck in IN_PROGRESS. Manual intervention required. Error: %s", + request.request_id, + final_error, + ) + else: + raise - with self.uow_factory.create_unit_of_work() as uow: - stuck_request = uow.requests.get_by_id(request.request_id) - if stuck_request: - stuck_request = stuck_request.update_status( - RequestStatus.FAILED, - "System error: failed to update status after double failure", - force=True, - ) - uow.requests.save(stuck_request) - except Exception as final_error: - self.logger.critical( - "CRITICAL: Failed to mark request %s as failed after double failure. " - "Request is stuck in IN_PROGRESS. Manual intervention required. Error: %s", - request.request_id, - final_error, - ) - # Nothing more we can do + async def _update_request_to_failed(self, request: Any, errors: list[str]) -> None: + """Update request status to failed.""" + error_message = "; ".join(errors) if errors else "Deprovisioning failed" + await self._update_request_status( + request, RequestStatus.FAILED, f"Return request failed: {error_message}" + ) + + async def _update_request_to_terminating(self, request: Any) -> None: + """Set return request to IN_PROGRESS after termination is accepted by provider. + + Termination is asynchronous — the provider accepted the request but instances + are still ``shutting-down``. Using IN_PROGRESS here lets the background sync + poll AWS and transition to COMPLETED only when all instances reach ``terminated``. + """ + await self._update_request_status( + request, + RequestStatus.IN_PROGRESS, + "Termination accepted: waiting for instances to reach terminated state", + ) async def _update_request_to_completed(self, request: Any) -> None: """Update return request status to completed and persist.""" try: - from orb.application.dto.commands import UpdateRequestStatusCommand - from orb.application.ports.command_bus_port import CommandBusPort - from orb.domain.request.request_types import RequestStatus - - update_command = UpdateRequestStatusCommand( - request_id=str(request.request_id), - status=RequestStatus.COMPLETED, - message="Return request completed: termination initiated", + await self._update_request_status( + request, + RequestStatus.COMPLETED, + "Return request completed: termination initiated", ) - command_bus = self._container.get(CommandBusPort) - await command_bus.execute(update_command) - self.logger.info("Updated request %s status to completed", request.request_id) except Exception as update_error: - self.logger.error( - "Failed to update request status to completed: %s", - update_error, - exc_info=True, - ) # Ensure the request reaches a terminal status so callers don't poll forever await self._update_request_to_failed( request, [f"Failed to mark completed: {update_error}"] diff --git a/src/orb/application/commands/request_sync_handlers.py b/src/orb/application/commands/request_sync_handlers.py index a8499d815..b255e7f40 100644 --- a/src/orb/application/commands/request_sync_handlers.py +++ b/src/orb/application/commands/request_sync_handlers.py @@ -170,7 +170,7 @@ async def execute_command(self, command: SyncRequestCommand) -> None: if new_status: await status_service.update_request_status( - request, new_status, status_message or "" + request, new_status, status_message or "", provider_metadata ) self.logger.info("Successfully synced request: %s", command.request_id) diff --git a/src/orb/application/factories/request_dto_factory.py b/src/orb/application/factories/request_dto_factory.py index 64b6829b9..a23cee194 100644 --- a/src/orb/application/factories/request_dto_factory.py +++ b/src/orb/application/factories/request_dto_factory.py @@ -1,7 +1,10 @@ """Request DTO factory for data transformation.""" +from typing import Optional + from orb.application.machine.result_mapping import map_machine_status_to_result from orb.application.request.dto import MachineReferenceDTO, RequestDTO +from orb.domain.base.provider_fulfilment import ProviderFulfilment from orb.domain.machine.aggregate import Machine from orb.domain.request.aggregate import Request from orb.domain.request.request_types import RequestType @@ -11,9 +14,21 @@ class RequestDTOFactory: """Factory for creating RequestDTOs from domain objects.""" def create_from_domain( - self, request: Request, machines: list[Machine] | None = None + self, + request: Request, + machines: list[Machine] | None = None, + fulfilment: Optional[ProviderFulfilment] = None, ) -> RequestDTO: - """Create RequestDTO from domain objects.""" + """Create RequestDTO from domain objects. + + Args: + request: Request domain aggregate. + machines: Optional list of Machine aggregates to embed as references. + fulfilment: Optional ProviderFulfilment to surface capacity fields + (target_units, fulfilled_units, running_count, pending_count). + When None, ``RequestDTO.from_domain`` falls back to + ``request.metadata["last_fulfilment"]`` if present. + """ if machines is None: machines = [] @@ -26,7 +41,7 @@ def create_from_domain( machine.status.value, request.request_type ), status=machine.status.value, - private_ip_address=machine.private_ip or "", + private_ip_address=machine.private_ip or None, public_ip_address=machine.public_ip, instance_type=str(machine.instance_type) if machine.instance_type else None, price_type=machine.price_type, @@ -41,7 +56,9 @@ def create_from_domain( ] # Create RequestDTO using existing factory method - return RequestDTO.from_domain(request, machine_references=machine_references) + return RequestDTO.from_domain( + request, machine_references=machine_references, fulfilment=fulfilment + ) def map_machine_status_to_result(self, status: str, request_type: RequestType) -> str: """Map machine status to result code.""" diff --git a/src/orb/application/machine/dto.py b/src/orb/application/machine/dto.py index 7b4c11bc4..94cc8cd3d 100644 --- a/src/orb/application/machine/dto.py +++ b/src/orb/application/machine/dto.py @@ -42,7 +42,7 @@ class MachineDTO(BaseDTO): security_group_ids: Optional[list[str]] = Field(default_factory=list) status_reason: Optional[str] = None termination_time: Optional[Union[int, str]] = None - tags: Optional[Any] = None + tags: Optional[dict[str, str]] = None provider_data: dict[str, Any] = Field(default_factory=dict) version: int = 0 @@ -108,7 +108,7 @@ def from_domain(cls, machine: Machine, timestamp_format: str = "auto") -> "Machi if machine.termination_time is not None else None ), - tags=machine.tags, + tags=machine.tags.to_dict() if machine.tags else None, ) def to_dict(self) -> dict[str, Any]: diff --git a/src/orb/application/queries/request_query_handlers.py b/src/orb/application/queries/request_query_handlers.py index defc7947d..077f2ded4 100644 --- a/src/orb/application/queries/request_query_handlers.py +++ b/src/orb/application/queries/request_query_handlers.py @@ -12,12 +12,14 @@ ListReturnRequestsQuery, ) from orb.application.dto.responses import RequestDTO +from orb.application.factories.request_dto_factory import RequestDTOFactory from orb.application.request.queries import ListRequestsQuery from orb.application.services.machine_sync_service import MachineSyncService from orb.application.services.provider_registry_service import ProviderRegistryService +from orb.application.services.request_query_service import RequestQueryService from orb.application.services.request_status_service import RequestStatusService from orb.domain.base import UnitOfWorkFactory -from orb.domain.base.exceptions import EntityNotFoundError +from orb.domain.base.exceptions import EntityNotFoundError, ProviderContractError from orb.domain.base.ports import ContainerPort, ErrorHandlingPort, LoggingPort from orb.domain.services.generic_filter_service import GenericFilterService @@ -44,9 +46,6 @@ def __init__( self._cache_service = self._get_cache_service() self.event_publisher = self._get_event_publisher() - from orb.application.factories.request_dto_factory import RequestDTOFactory - from orb.application.services.request_query_service import RequestQueryService - self._query_service = RequestQueryService(uow_factory, logger) self._status_service = RequestStatusService(uow_factory, logger) self._dto_factory = RequestDTOFactory() @@ -80,6 +79,13 @@ async def execute_query(self, query: GetRequestQuery) -> RequestDTO: # no domain invariants are enforced here, no domain events are raised. # The alternative (background polling) requires infrastructure that doesn't # exist yet. If you want to remove this, implement a background sync first. + # + # Terminal requests are immutable — skip the provider describe round-trip + # and return the persisted machines directly. + db_machines: list = [] + if request.status.is_terminal(): + db_machines = await self._query_service.get_machines_for_request(request) + return self._dto_factory.create_from_domain(request, db_machines) try: await self._machine_sync_service.populate_missing_machine_ids(request) db_machines = await self._query_service.get_machines_for_request(request) @@ -95,16 +101,18 @@ async def execute_query(self, query: GetRequestQuery) -> RequestDTO: ) if new_status: await self._status_service.update_request_status( - request, new_status, status_message or "" + request, new_status, status_message or "", provider_metadata ) request = await self._query_service.get_request(query.request_id) + except ProviderContractError: + raise except Exception as sync_err: self.logger.warning( "Error syncing request %s, returning stored state: %s", query.request_id, sync_err, ) - return self._dto_factory.create_from_domain(request, []) + return self._dto_factory.create_from_domain(request, db_machines) # Deliberate query-time mutation: record when this request was polled for status. # first_status_check is set once; last_status_check is updated on every poll. @@ -116,7 +124,17 @@ async def execute_query(self, query: GetRequestQuery) -> RequestDTO: machine_objects = await self._query_service.get_machines_for_request(request) request_dto = self._dto_factory.create_from_domain(request, machine_objects) - if self._cache_service and self._cache_service.is_caching_enabled(): + # Only cache terminal requests. Non-terminal requests (in_progress, + # pending, provisioning) must be re-synced against the provider on + # every poll so that status transitions (→ completed, → failed) are + # picked up promptly. Caching in_progress responses causes the + # poll loop to receive stale status for the full TTL window and the + # request appears stuck until the cache expires. + if ( + self._cache_service + and self._cache_service.is_caching_enabled() + and request.status.is_terminal() + ): self._cache_service.cache_request(query.request_id, request_dto) self.logger.info("Retrieved request: %s", query.request_id) @@ -206,15 +224,13 @@ async def execute_query(self, query: ListRequestsQuery) -> list[RequestDTO]: end_idx = start_idx + (query.limit or 50) requests = requests[start_idx:end_idx] + dto_factory = RequestDTOFactory() request_dtos = [] for request in requests: machines = [] if request.machine_ids: machines = uow.machines.find_by_ids(request.machine_ids) - from orb.application.factories.request_dto_factory import RequestDTOFactory - - dto_factory = RequestDTOFactory() request_dto = dto_factory.create_from_domain(request, machines) request_dtos.append(request_dto) @@ -243,16 +259,65 @@ def __init__( logger: LoggingPort, error_handler: ErrorHandlingPort, generic_filter_service: GenericFilterService, + machine_sync_service: MachineSyncService, ) -> None: super().__init__(logger, error_handler) self.uow_factory = uow_factory self._generic_filter_service = generic_filter_service + self._machine_sync_service = machine_sync_service + self._status_service = RequestStatusService(uow_factory, logger) + self._query_service = RequestQueryService(uow_factory, logger) + self._dto_factory = RequestDTOFactory() async def execute_query(self, query: ListReturnRequestsQuery) -> list[RequestDTO]: """Execute list return requests query.""" self.logger.info("Listing return requests") try: + with self.uow_factory.create_unit_of_work() as uow: + from orb.domain.request.value_objects import RequestType + + return_requests = uow.requests.find_by_type(RequestType.RETURN) + + # Read-through sync: refresh each non-terminal return request from + # live provider state so a return that has actually completed at the + # provider transitions to COMPLETED in the DB. Without this, a + # caller polling list_return_requests would see a request stuck in + # IN_PROGRESS forever and may retry the return — triggering + # provider-side double-decrement (e.g. ASG capacity off by one). + for request in return_requests: + if request.status.is_terminal(): + continue + try: + db_machines = await self._query_service.get_machines_for_request(request) + ( + provider_machines, + provider_metadata, + ) = await self._machine_sync_service.fetch_provider_machines( + request, db_machines + ) + ( + synced_machines, + _, + ) = await self._machine_sync_service.sync_machines_with_provider( + request, db_machines, provider_machines + ) + new_status, status_message = ( + self._status_service.determine_status_from_machines( + db_machines, synced_machines, request, provider_metadata + ) + ) + if new_status: + await self._status_service.update_request_status( + request, new_status, status_message or "", provider_metadata + ) + except Exception as sync_err: + self.logger.warning( + "Sync failed for return request %s, returning stored state: %s", + request.request_id.value, + sync_err, + ) + with self.uow_factory.create_unit_of_work() as uow: from orb.domain.request.value_objects import RequestType @@ -264,10 +329,7 @@ async def execute_query(self, query: ListReturnRequestsQuery) -> list[RequestDTO if request.machine_ids: machines = uow.machines.find_by_ids(request.machine_ids) - from orb.application.factories.request_dto_factory import RequestDTOFactory - - dto_factory = RequestDTOFactory() - request_dto = dto_factory.create_from_domain(request, machines) + request_dto = self._dto_factory.create_from_domain(request, machines) request_dtos.append(request_dto) if query.machine_names: @@ -330,10 +392,8 @@ def __init__( self._generic_filter_service = generic_filter_service self._machine_sync_service = machine_sync_service self._status_service = RequestStatusService(uow_factory, logger) - - from orb.application.services.request_query_service import RequestQueryService - self._query_service = RequestQueryService(uow_factory, logger) + self._dto_factory = RequestDTOFactory() async def execute_query(self, query: ListActiveRequestsQuery) -> list[RequestDTO]: """Execute list active requests query.""" @@ -383,7 +443,7 @@ async def execute_query(self, query: ListActiveRequestsQuery) -> list[RequestDTO ) if new_status: await self._status_service.update_request_status( - request, new_status, status_message or "" + request, new_status, status_message or "", provider_metadata ) except Exception as sync_err: self.logger.warning( @@ -397,10 +457,7 @@ async def execute_query(self, query: ListActiveRequestsQuery) -> list[RequestDTO request = await self._query_service.get_request(str(request.request_id.value)) db_machines = await self._query_service.get_machines_for_request(request) - from orb.application.factories.request_dto_factory import RequestDTOFactory - - dto_factory = RequestDTOFactory() - request_dto = dto_factory.create_from_domain(request, db_machines) + request_dto = self._dto_factory.create_from_domain(request, db_machines) request_dtos.append(request_dto) if query.filter_expressions: diff --git a/src/orb/application/request/dto.py b/src/orb/application/request/dto.py index 897561f3d..4c05adce3 100644 --- a/src/orb/application/request/dto.py +++ b/src/orb/application/request/dto.py @@ -7,6 +7,7 @@ from orb.application.dto.base import BaseDTO from orb.application.machine.result_mapping import map_machine_status_to_result +from orb.domain.base.provider_fulfilment import ProviderFulfilment from orb.domain.machine.aggregate import Machine from orb.domain.request.aggregate import Request from orb.domain.request.request_types import RequestType @@ -19,7 +20,7 @@ class MachineReferenceDTO(BaseDTO): name: str = "" result: str # 'executing', 'fail', or 'succeed' status: str - private_ip_address: str = "" + private_ip_address: Optional[str] = None public_ip_address: Optional[str] = None # Already using the expected API field name instance_type: Optional[str] = None price_type: Optional[str] = None @@ -54,7 +55,7 @@ def from_machine(cls, machine: Machine, request_type: RequestType) -> "MachineRe name=machine.display_name, result=map_machine_status_to_result(status, request_type=rt_str), status=status, - private_ip_address=machine.private_ip or "", + private_ip_address=machine.private_ip or None, public_ip_address=machine.public_ip, instance_type=str(machine.instance_type) if machine.instance_type else None, price_type=machine.price_type, @@ -115,6 +116,17 @@ class RequestDTO(BaseDTO): provider_data: dict[str, Any] = Field(default_factory=dict) version: int = 0 resource_ids: list[str] = Field(default_factory=list) + # Structured error block surfaced when status is failed or partial. + # Populated from error_details["provider_error"] (or legacy "aws_error") when + # an AWS API error was captured. + error: Optional[dict[str, Any]] = None + # Capacity fields from ProviderFulfilment — present when the provider emits them. + # For weighted fleets target_units and fulfilled_units reflect capacity units; + # for RunInstances they reflect instance count (1 unit = 1 instance). + target_units: Optional[int] = None + fulfilled_units: Optional[int] = None + running_count: Optional[int] = None + pending_count: Optional[int] = None @classmethod def from_domain( @@ -122,6 +134,7 @@ def from_domain( request: Request, verbose: bool = False, machine_references: Optional[list["MachineReferenceDTO"]] = None, + fulfilment: Optional[ProviderFulfilment] = None, ) -> "RequestDTO": """ Create DTO from domain object. @@ -146,6 +159,26 @@ def from_domain( MachineReferenceDTO.from_machine(m, request.request_type) for m in domain_refs ] + # Resolve the ProviderFulfilment to use for capacity fields. + # Priority: explicit arg > cached snapshot in metadata["last_fulfilment"]. + resolved_fulfilment: Optional[ProviderFulfilment] = fulfilment + if resolved_fulfilment is None and request.metadata: + cached = request.metadata.get("last_fulfilment") + if isinstance(cached, dict): + try: + resolved_fulfilment = ProviderFulfilment(**cached) + except (TypeError, KeyError): + resolved_fulfilment = None + + # Build structured error block from error_details when available. + # Accept both "provider_error" (new) and legacy "aws_error" so that + # records persisted before this deploy continue to surface the error block. + error_block: Optional[dict[str, Any]] = None + if request.error_details: + error_block = request.error_details.get("provider_error") or request.error_details.get( + "aws_error" + ) + # Create the DTO with all available fields return cls( request_id=str(request.request_id), @@ -177,6 +210,13 @@ def from_domain( provider_data=request.provider_data, version=request.version, resource_ids=request.resource_ids, + error=error_block, + # Capacity fields — populated from resolved ProviderFulfilment when available. + # Falls back to metadata["last_fulfilment"] when no explicit arg is supplied. + target_units=resolved_fulfilment.target_units if resolved_fulfilment else None, + fulfilled_units=resolved_fulfilment.fulfilled_units if resolved_fulfilment else None, + running_count=resolved_fulfilment.running_count if resolved_fulfilment else None, + pending_count=resolved_fulfilment.pending_count if resolved_fulfilment else None, ) def to_dict(self, verbose: bool = False) -> dict[str, Any]: @@ -203,6 +243,17 @@ def to_dict(self, verbose: bool = False) -> dict[str, Any]: # Remove machine_references field as it's replaced by machines result.pop("machine_references", None) + # Include error block only when present (failed / partial requests). + if self.error: + result["error"] = self.error + else: + result.pop("error", None) + + # Include capacity fields only when populated (fleet/weighted-capacity requests). + for cap_field in ("target_units", "fulfilled_units", "running_count", "pending_count"): + if result.get(cap_field) is None: + result.pop(cap_field, None) + # Remove fields based on detail level if not include_details: result.pop("metadata", None) diff --git a/src/orb/application/services/machine_sync_service.py b/src/orb/application/services/machine_sync_service.py index 8f58705cd..477da7ec6 100644 --- a/src/orb/application/services/machine_sync_service.py +++ b/src/orb/application/services/machine_sync_service.py @@ -80,6 +80,7 @@ async def fetch_provider_machines( "resource_ids": request.resource_ids, "provider_api": request.provider_api, "template_id": request.template_id, + "requested_count": request.requested_count, } # Fallback to instance-level discovery for requests without resource tracking elif db_machines: @@ -261,6 +262,7 @@ async def sync_machines_with_provider( or existing.vpc_id != provider_machine.vpc_id or existing.status_reason != provider_machine.status_reason or existing.provider_data != provider_machine.provider_data + or (provider_machine.resource_id and not existing.resource_id) ) # Debug logging @@ -288,6 +290,8 @@ async def sync_machines_with_provider( machine_data["vpc_id"] = provider_machine.vpc_id machine_data["version"] = existing.version + 1 machine_data["tags"] = provider_machine.tags + if provider_machine.resource_id and not existing.resource_id: + machine_data["resource_id"] = provider_machine.resource_id updated_machine = Machine.model_validate(machine_data) to_upsert.append(updated_machine) diff --git a/src/orb/application/services/orchestration/dtos.py b/src/orb/application/services/orchestration/dtos.py index 9a9a650ed..06a69365d 100644 --- a/src/orb/application/services/orchestration/dtos.py +++ b/src/orb/application/services/orchestration/dtos.py @@ -73,6 +73,7 @@ class ReturnMachinesOutput: status: str message: str = "" skipped_machines: list[str] = dataclasses.field(default_factory=list) + machine_ids: list[str] = dataclasses.field(default_factory=list) @dataclasses.dataclass(frozen=True) @@ -155,8 +156,8 @@ class GetTemplateOutput: @dataclasses.dataclass(frozen=True) class CreateTemplateInput: template_id: str - provider_api: str image_id: str + provider_api: Optional[str] = None name: Optional[str] = None description: Optional[str] = None instance_type: Optional[str] = None diff --git a/src/orb/application/services/orchestration/return_machines.py b/src/orb/application/services/orchestration/return_machines.py index 6ee743774..19dbc8033 100644 --- a/src/orb/application/services/orchestration/return_machines.py +++ b/src/orb/application/services/orchestration/return_machines.py @@ -108,6 +108,7 @@ async def execute(self, input: ReturnMachinesInput) -> ReturnMachinesOutput: # return ReturnMachinesOutput( request_id=primary_request_id, status=status, + machine_ids=machine_ids, ) async def _poll_until_terminal(self, request_id: str, timeout_seconds: int) -> str: diff --git a/src/orb/application/services/provider_validation_service.py b/src/orb/application/services/provider_validation_service.py index 2decb9225..bb9245a12 100644 --- a/src/orb/application/services/provider_validation_service.py +++ b/src/orb/application/services/provider_validation_service.py @@ -10,7 +10,6 @@ from orb.domain.template.template_aggregate import Template from orb.domain.base.ports import ContainerPort, LoggingPort, ProviderSelectionPort -from orb.domain.base.ports.configuration_port import ConfigurationPort from orb.domain.base.ports.provider_validation_port import ProviderValidationPort from orb.domain.base.results import ProviderSelectionResult @@ -30,25 +29,6 @@ def __init__( self._provider_selection_port = provider_selection_port self._validator = validator - async def validate_provider_availability(self) -> None: - """Validate that providers are available.""" - config_manager = self._container.get(ConfigurationPort) - provider_config = config_manager.get_provider_config() - - if provider_config: - for provider_instance in provider_config.get_active_providers(): - self._provider_selection_port.register_provider_strategy( - provider_instance.type, provider_instance - ) - - available_strategies = self._provider_selection_port.get_available_strategies() - if not available_strategies: - error_msg = "No provider strategies available - cannot create machine requests" - self.logger.error(error_msg) - raise ValueError(error_msg) - - self.logger.debug("Available provider strategies: %s", available_strategies) - async def select_and_validate_provider(self, template: Template) -> ProviderSelectionResult: """Select provider and validate template compatibility.""" selection_result = self._provider_selection_port.select_provider_for_template(template) diff --git a/src/orb/application/services/provisioning_orchestration_service.py b/src/orb/application/services/provisioning_orchestration_service.py index 4136f1f67..388fc378a 100644 --- a/src/orb/application/services/provisioning_orchestration_service.py +++ b/src/orb/application/services/provisioning_orchestration_service.py @@ -1,14 +1,22 @@ """Service for orchestrating provider provisioning operations.""" -from dataclasses import dataclass +import asyncio +from dataclasses import dataclass, field from datetime import datetime, timezone -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, assert_never if TYPE_CHECKING: from orb.domain.base.ports.provider_selection_port import ProviderSelectionPort from orb.infrastructure.resilience.strategy.circuit_breaker import CircuitBreakerStrategy from orb.domain.base.exceptions import QuotaError +from orb.domain.base.operation_outcome import ( + Accepted, + Completed, + Failed, + OperationOutcome, + RequiresFollowUp, +) from orb.domain.base.ports import ConfigurationPort, ContainerPort, LoggingPort, ProviderConfigPort from orb.domain.base.results import ProviderSelectionResult from orb.domain.request.aggregate import Request @@ -19,7 +27,24 @@ @dataclass class ProvisioningResult: - """Result of provisioning operation.""" + """Result of provisioning operation. + + The ``outcome`` field carries the typed :class:`OperationOutcome` returned + by the provider. ``is_final`` is preserved for backward compatibility and + is derived from ``outcome`` when it is present. + + ``Accepted`` → ``is_final = False`` (provider is still processing) + ``Completed`` → ``is_final = True`` + ``RequiresFollowUp`` → ``is_final = False`` (background work remains) + ``Failed`` → ``is_final = True`` + ``None`` (legacy) → honour the explicit ``is_final`` value + + Provider error fields (all optional, only set on failure): + ``provider_error_code`` — provider API error code (e.g. ``UnauthorizedOperation``) + ``provider_error_message`` — human-readable message from the provider response + ``provider_request_id`` — provider request ID for support cases + ``error_source`` — service.operation label (e.g. ``aws.ec2.RunInstances``) + """ success: bool resource_ids: list[str] @@ -29,6 +54,47 @@ class ProvisioningResult: error_message: str | None = None fulfilled_count: int = 0 is_final: bool = True + outcome: OperationOutcome | None = field(default=None) + # Provider error detail fields — populated when the failure originates from + # a provider API call so callers can surface actionable diagnostics to the user. + provider_error_code: str | None = None + provider_error_message: str | None = None + provider_request_id: str | None = None + error_source: str | None = None + + def __post_init__(self) -> None: + """Derive ``is_final`` from ``outcome`` when an outcome is provided.""" + if self.outcome is not None: + match self.outcome: + case Accepted(): + self.is_final = False + case Completed(): + self.is_final = True + case RequiresFollowUp(): + self.is_final = False + case Failed(): + self.is_final = True + case _ as unreachable: + assert_never(unreachable) + + +def _extract_aws_error_fields(exc: BaseException) -> dict[str, Any]: + """Extract provider error fields from a provider exception (if applicable). + + Returns a dict suitable for **-unpacking into ProvisioningResult. When the + exception carries no provider error attributes the dict will contain only + None values so the ProvisioningResult fields stay empty (safe default). + """ + provider_error_code: str | None = getattr(exc, "aws_error_code", None) + provider_error_message: str | None = getattr(exc, "aws_error_message", None) + provider_request_id: str | None = getattr(exc, "aws_request_id", None) + error_source: str | None = getattr(exc, "error_source", None) + return { + "provider_error_code": provider_error_code, + "provider_error_message": provider_error_message, + "provider_request_id": provider_request_id, + "error_source": error_source, + } class ProvisioningOrchestrationService: @@ -58,12 +124,14 @@ async def execute_provisioning( default_config: dict[str, Any] = { "max_retries": request_config.get("fulfillment_max_retries", 3), "timeout_seconds": request_config.get("fulfillment_timeout_seconds", 300), + "dispatch_timeout_seconds": request_config.get("dispatch_timeout_seconds", 300), "batch_size": request_config.get("fulfillment_batch_size", 1000), "fallback_template_id": request_config.get("fulfillment_fallback_template_id"), } config = {**default_config, **request.metadata.get("fulfillment_config", {})} max_retries: int = int(config["max_retries"]) timeout_seconds: float = float(config["timeout_seconds"]) + dispatch_timeout_seconds: float = float(config["dispatch_timeout_seconds"]) batch_size: int = int(config["batch_size"]) started_at = datetime.now(timezone.utc) @@ -104,7 +172,7 @@ async def execute_provisioning( try: last_result = await self._dispatch_single_attempt( - template, request, selection_result, attempt_count + template, request, selection_result, attempt_count, dispatch_timeout_seconds ) except Exception as e: if not isinstance(e, CircuitBreakerOpenError): @@ -168,7 +236,7 @@ async def execute_provisioning( request.requested_count, remaining, ) - request, persist_ok = self._persist_acquiring(request) + request, persist_ok = await asyncio.to_thread(self._persist_acquiring, request) if not persist_ok: self._logger.warning( "ACQUIRING persist failed for request %s on attempt %d — " @@ -249,6 +317,7 @@ async def _dispatch_single_attempt( request: Request, selection_result: ProviderSelectionResult, count: int, + dispatch_timeout_seconds: float = 300.0, ) -> ProvisioningResult: """Dispatch a single provisioning attempt for `count` instances.""" try: @@ -277,9 +346,10 @@ async def _dispatch_single_attempt( self._provider_config_port.get_provider_instance_config(selection_result.provider_name) - result = await self._provider_selection_port.execute_operation( - selection_result.provider_name, operation - ) + async with asyncio.timeout(dispatch_timeout_seconds): + result = await self._provider_selection_port.execute_operation( + selection_result.provider_name, operation + ) if result.success: self._logger.debug("Provider result.data: %s", result.data) @@ -300,6 +370,25 @@ async def _dispatch_single_attempt( if result.routing_info: merged_provider_data.update(result.routing_info) + # Build a typed OperationOutcome from the ProviderResult payload. + # AWS provider returns request-IDs with pending instances (async + # model) — express that as Accepted rather than pretending it is + # immediately Completed. + is_immediately_final = ( + not has_capacity_error and len(instances) >= count + ) or fulfillment_final + if is_immediately_final: + outcome: OperationOutcome = Completed( + resource_ids=resource_ids, + metadata=merged_provider_data, + ) + else: + outcome = Accepted( + request_id=str(request.request_id), + pending_resource_ids=resource_ids, + metadata=merged_provider_data, + ) + self._record_provider_success(selection_result.provider_name) return ProvisioningResult( success=True, @@ -308,8 +397,8 @@ async def _dispatch_single_attempt( instances=instances, provider_data=merged_provider_data, fulfilled_count=len(instances), - is_final=(not has_capacity_error and len(instances) >= count) - or fulfillment_final, + outcome=outcome, + # is_final derived from outcome in __post_init__ ) else: return ProvisioningResult( @@ -319,12 +408,35 @@ async def _dispatch_single_attempt( instances=[], provider_data=result.metadata or {}, error_message=result.error_message, + outcome=Failed( + error=result.error_message or "Provider returned failure", + recoverable=False, + ), ) except CircuitBreakerOpenError: raise # do not swallow — let it propagate to execute_provisioning + except TimeoutError: + timeout_msg = f"Dispatch timed out after {dispatch_timeout_seconds:.0f}s" + self._logger.warning( + "Dispatch timed out after %.1fs for provider %s (request %s)", + dispatch_timeout_seconds, + selection_result.provider_name, + str(request.request_id) if hasattr(request, "request_id") else "unknown", + ) + return ProvisioningResult( + success=False, + resource_ids=[], + machine_ids=[], + instances=[], + provider_data={}, + error_message=timeout_msg, + outcome=Failed(error=timeout_msg, recoverable=True), + ) + except QuotaError as e: + quota_msg = f"Quota exceeded: {e}" self._logger.error( "Quota error during provisioning for template %s: %s", template.template_id if hasattr(template, "template_id") else "unknown", @@ -337,17 +449,20 @@ async def _dispatch_single_attempt( "error_type": type(e).__name__, }, ) + aws_fields = _extract_aws_error_fields(e) return ProvisioningResult( success=False, resource_ids=[], machine_ids=[], instances=[], provider_data={}, - error_message=f"Quota exceeded: {e}", - is_final=True, + error_message=quota_msg, + outcome=Failed(error=quota_msg, recoverable=False), + **aws_fields, ) except Exception as e: + generic_msg = f"Provisioning failed: {e}" self._logger.error( "Provisioning dispatch failed for template %s: %s", template.template_id if hasattr(template, "template_id") else "unknown", @@ -361,11 +476,14 @@ async def _dispatch_single_attempt( "error_type": type(e).__name__, }, ) + aws_fields = _extract_aws_error_fields(e) return ProvisioningResult( success=False, resource_ids=[], machine_ids=[], instances=[], provider_data={}, - error_message=f"Provisioning failed: {e}", + error_message=generic_msg, + outcome=Failed(error=generic_msg, recoverable=False), + **aws_fields, ) diff --git a/src/orb/application/services/request_follow_up_context.py b/src/orb/application/services/request_follow_up_context.py new file mode 100644 index 000000000..f634e07f0 --- /dev/null +++ b/src/orb/application/services/request_follow_up_context.py @@ -0,0 +1,17 @@ +"""Re-export shim — FollowUpContext types live in the domain layer. + +Import from ``orb.domain.base.follow_up_context`` directly for new code. +This module exists only for backward compatibility. +""" + +from orb.domain.base.follow_up_context import ( # noqa: F401 + DeploymentPollingFollowUpContext, + FollowUpContext, + TerminationFollowUpContext, +) + +__all__ = [ + "DeploymentPollingFollowUpContext", + "FollowUpContext", + "TerminationFollowUpContext", +] diff --git a/src/orb/application/services/request_status_management_service.py b/src/orb/application/services/request_status_management_service.py index b45041add..b15e0ca5a 100644 --- a/src/orb/application/services/request_status_management_service.py +++ b/src/orb/application/services/request_status_management_service.py @@ -81,7 +81,7 @@ async def update_request_from_provisioning( ) def _handle_provisioning_failure(self, request: Any, provisioning_result: Any) -> Any: - """Handle provisioning failure.""" + """Handle provisioning failure, capturing provider error details when available.""" from orb.domain.request.value_objects import RequestStatus error_message = ( @@ -95,6 +95,41 @@ def _handle_provisioning_failure(self, request: Any, provisioning_result: Any) - {"error_message": error_message, "error_type": "ProvisioningFailure"} ) + # Persist structured provider error details so they are available to the status + # response. Only non-None fields are included to keep error_details clean. + provider_error_code: str | None = getattr(provisioning_result, "provider_error_code", None) + provider_error_message: str | None = getattr( + provisioning_result, "provider_error_message", None + ) + provider_request_id: str | None = getattr(provisioning_result, "provider_request_id", None) + error_source: str | None = getattr(provisioning_result, "error_source", None) + + if any([provider_error_code, provider_error_message, provider_request_id, error_source]): + aws_error_block: dict[str, Any] = {} + if provider_error_code: + aws_error_block["code"] = provider_error_code + if provider_error_message: + aws_error_block["message"] = provider_error_message + if error_source: + aws_error_block["source"] = error_source + if provider_request_id: + aws_error_block["aws_request_id"] = provider_request_id + + # Merge into error_details so it survives serialization / persistence. + current = dict(request.error_details) if request.error_details else {} + current["provider_error"] = aws_error_block + # Pydantic freeze-safe: use model_copy for the error_details field. + from orb.domain.request.aggregate import Request as RequestAggregate + + if isinstance(request, RequestAggregate): + fields = request.model_dump() + fields["error_details"] = current + fields["version"] = request.version + 1 + request = RequestAggregate.model_validate(fields) + else: + # Fallback for mock objects in tests + request.error_details = current # type: ignore[attr-defined] + return request def _update_request_status( diff --git a/src/orb/application/services/request_status_service.py b/src/orb/application/services/request_status_service.py index cbdbe9e02..35299f910 100644 --- a/src/orb/application/services/request_status_service.py +++ b/src/orb/application/services/request_status_service.py @@ -1,9 +1,29 @@ -"""Request status service for business logic.""" - +"""Request status service for business logic. + +Acquire path (fulfilment-based) +--------------------------------- +The application layer trusts the provider's ``ProviderFulfilment`` verdict +exclusively. No count math. No provider-specific key inspection. + +Every provider's ``check_hosts_status`` MUST return a ``CheckHostsStatusResult`` +with a ``ProviderFulfilment``. If the fulfilment is missing the service raises +``ProviderContractError`` — a hard error, not a silent fallback. + +Return path +----------- +``determine_status_from_machines`` still uses the existing machine-status +counting for return requests because termination is observable via instance +states (shutting-down → terminated) without a fleet-level capacity concept. +The return path is unchanged. +""" + +import dataclasses from typing import Optional, Tuple from orb.domain.base import UnitOfWorkFactory +from orb.domain.base.exceptions import ProviderContractError from orb.domain.base.ports.logging_port import LoggingPort +from orb.domain.base.provider_fulfilment import ProviderFulfilment from orb.domain.machine.aggregate import Machine from orb.domain.request.aggregate import Request from orb.domain.request.request_types import RequestStatus, RequestType @@ -27,137 +47,158 @@ def determine_status_from_machines( request: Request, provider_metadata: dict, ) -> Tuple[Optional[str], Optional[str]]: - """Determine request status from machine states.""" - try: - db_machine_count = len(db_machines) + """Determine request status from machine states. - # Determine new status based on request type - if request.request_type.value == "return": - # For return requests: empty provider_machines means instances are gone from AWS → COMPLETED - if not provider_machines: - return ( - RequestStatus.COMPLETED.value, - f"Return request completed: all machines terminated " - f"(no longer visible in provider) (total in DB: {db_machine_count})", - ) + For acquire requests the provider MUST supply a ``ProviderFulfilment`` + via ``provider_metadata["provider_fulfilment"]``. Any legacy + ``fleet_capacity_fulfilment`` key is ignored — the provider contract + is the only truth. - shutting_down_count = sum( - 1 for m in provider_machines if m.status.value in ["shutting-down", "stopping"] - ) - terminated_count = sum( - 1 for m in provider_machines if m.status.value in ["terminated", "stopped"] + For return requests the existing machine-state counting logic is used. + """ + try: + if request.request_type.value == "return": + return self._determine_return_status( + db_machines, provider_machines, request, provider_metadata ) - running_count = sum(1 for m in provider_machines if m.status.value == "running") - failed_count = sum(1 for m in provider_machines if m.status.value == "failed") - total_count = len(provider_machines) - - # shutting-down/stopping are transient — only terminated/stopped are truly done - effectively_done_count = terminated_count - if effectively_done_count == total_count and running_count == 0: - return ( - RequestStatus.COMPLETED.value, - f"Return request completed: {terminated_count} terminated, " - f"{shutting_down_count} shutting down " - f"(total in DB: {db_machine_count})", - ) - elif running_count > 0: - return ( - RequestStatus.IN_PROGRESS.value, - f"Return in progress: {running_count} machines still running, " - f"awaiting termination (total in DB: {db_machine_count})", - ) - elif failed_count > 0: - return ( - RequestStatus.FAILED.value, - f"Return request failed: {failed_count} machines failed to terminate " - f"(total in DB: {db_machine_count})", - ) - else: - return RequestStatus.IN_PROGRESS.value, "Instances terminating" - # Acquisition request logic else: - machines_to_check = provider_machines if provider_machines else db_machines - - if not machines_to_check: - return ( - RequestStatus.IN_PROGRESS.value, - "Status determination failed — will retry", - ) - - running_count = sum(1 for m in machines_to_check if m.status.value == "running") - pending_count = sum( - 1 for m in machines_to_check if m.status.value in ["pending", "starting"] - ) - failed_count = sum(1 for m in machines_to_check if m.status.value == "failed") - total_count = len(machines_to_check) - - # Use fleet capacity metrics when available (fleets/ASGs report their own - # target and fulfilled capacity). Fall back to instance counts otherwise. - fleet_capacity = provider_metadata.get("fleet_capacity_fulfilment") or {} - effective_target = ( - fleet_capacity.get("target_capacity_units") or request.requested_count - ) - fulfilled_from_metadata = fleet_capacity.get("fulfilled_capacity_units") - effective_fulfilled: float = ( - float(fulfilled_from_metadata) - if fulfilled_from_metadata is not None - else float(running_count + pending_count) + return self._determine_acquire_status( + db_machines, provider_machines, request, provider_metadata ) - - # Fleet metadata (FulfilledCapacity) can lag or use floating-point values - # that don't exactly match target. Use running instance count as the - # authoritative signal when it meets the requested count. - fulfillment_final = fleet_capacity.get("fulfillment_final", False) - fleet_errors = provider_metadata.get("fleet_errors") or [] - - instance_target = request.requested_count - if (running_count >= instance_target and failed_count == 0) or ( - effective_fulfilled >= effective_target and failed_count == 0 - ): - return RequestStatus.COMPLETED.value, "All instances running successfully" - elif fulfillment_final and pending_count == 0: - error_detail = ( - f": {'; '.join(e.get('error_code', '') for e in fleet_errors if e.get('error_code'))}" - if fleet_errors - else "" - ) - if running_count > 0: - return ( - RequestStatus.PARTIAL.value, - f"{running_count}/{instance_target} instances running{error_detail}", - ) - else: - return RequestStatus.FAILED.value, f"All instances failed{error_detail}" - elif failed_count == total_count and total_count > 0: - return RequestStatus.FAILED.value, "All instances failed" - elif pending_count > 0: - return ( - RequestStatus.IN_PROGRESS.value, - f"{running_count}/{effective_target} instances running, waiting for {pending_count} more", - ) - elif running_count > 0 and (running_count + failed_count) >= effective_target: - return ( - RequestStatus.PARTIAL.value, - f"{running_count}/{effective_target} instances running", - ) - elif running_count > 0: - return ( - RequestStatus.IN_PROGRESS.value, - f"{running_count}/{effective_target} instances running, waiting for more", - ) - else: - return RequestStatus.IN_PROGRESS.value, "Instances starting" - + except ProviderContractError: + raise except Exception as e: self.logger.error(f"Failed to determine status from machines: {e}") return RequestStatus.IN_PROGRESS.value, "Status determination failed — will retry" - async def update_request_status(self, request: Request, status: str, message: str) -> Request: - """Update request status.""" + # ------------------------------------------------------------------ + # Acquire path — trusts ProviderFulfilment exclusively + # ------------------------------------------------------------------ + + def _determine_acquire_status( + self, + db_machines: list[Machine], + provider_machines: list[Machine], + request: Request, + provider_metadata: dict, + ) -> Tuple[Optional[str], Optional[str]]: + """Map ProviderFulfilment state to RequestStatus for acquire requests.""" + fulfilment: Optional[ProviderFulfilment] = provider_metadata.get("provider_fulfilment") + + if fulfilment is None: + raise ProviderContractError( + f"Provider {getattr(request, 'provider_name', 'unknown')} did not emit " + "ProviderFulfilment for acquire request. Every provider's " + "check_hosts_status must return CheckHostsStatusResult with fulfilment." + ) + + state_map: dict[str, str] = { + "fulfilled": RequestStatus.COMPLETED.value, + "in_progress": RequestStatus.IN_PROGRESS.value, + "partial": RequestStatus.PARTIAL.value, + "failed": RequestStatus.FAILED.value, + } + mapped = state_map.get(fulfilment.state) + if mapped is None: + # Unknown state — treat as in_progress to be safe + self.logger.warning( + "Unknown fulfilment state '%s', treating as in_progress", fulfilment.state + ) + return RequestStatus.IN_PROGRESS.value, fulfilment.message + + return mapped, fulfilment.message + + # ------------------------------------------------------------------ + # Return path — machine-state counting (unchanged) + # ------------------------------------------------------------------ + + def _determine_return_status( + self, + db_machines: list[Machine], + provider_machines: list[Machine], + request: Request, + provider_metadata: dict, + ) -> Tuple[Optional[str], Optional[str]]: + """Determine return request status from machine termination states.""" + db_machine_count = len(db_machines) + + # For return requests: empty provider_machines means instances are gone from AWS. + if not provider_machines: + return ( + RequestStatus.COMPLETED.value, + f"Return request completed: all machines terminated " + f"(no longer visible in provider) (total in DB: {db_machine_count})", + ) + + shutting_down_count = sum( + 1 for m in provider_machines if m.status.value in ["shutting-down", "stopping"] + ) + terminated_count = sum( + 1 for m in provider_machines if m.status.value in ["terminated", "stopped"] + ) + running_count = sum(1 for m in provider_machines if m.status.value == "running") + failed_count = sum(1 for m in provider_machines if m.status.value == "failed") + + # Compare against the number of machines the caller submitted for return. + completion_target = request.requested_count + + effectively_done_count = terminated_count + if effectively_done_count >= completion_target and running_count == 0: + return ( + RequestStatus.COMPLETED.value, + f"Return request completed: {terminated_count} terminated, " + f"{shutting_down_count} shutting down " + f"(total in DB: {db_machine_count})", + ) + elif running_count > 0: + return ( + RequestStatus.IN_PROGRESS.value, + f"Return in progress: {running_count} machines still running, " + f"awaiting termination (total in DB: {db_machine_count})", + ) + elif failed_count > 0: + return ( + RequestStatus.FAILED.value, + f"Return request failed: {failed_count} machines failed to terminate " + f"(total in DB: {db_machine_count})", + ) + else: + return RequestStatus.IN_PROGRESS.value, "Instances terminating" + + async def update_request_status( + self, + request: Request, + status: str, + message: str, + provider_metadata: Optional[dict] = None, + ) -> Request: + """Update request status and optionally cache the latest ProviderFulfilment. + + When ``provider_metadata`` is supplied and contains a ``provider_fulfilment`` + key, the fulfilment is serialised and stored as ``request.metadata["last_fulfilment"]``. + That snapshot is later read by ``RequestDTO.from_domain`` so capacity fields + (target_units, fulfilled_units, running_count, pending_count) appear in every + response without requiring the caller to re-pass the fulfilment explicitly. + + No-op if the request is already in a terminal state. Terminal requests + (COMPLETED/FAILED/CANCELLED/TIMEOUT/PARTIAL) are immutable; re-evaluation + of a terminal request during a query-time sync must not attempt to + downgrade it. + """ + if request.status.is_terminal(): + return request try: status_enum = RequestStatus(status) updated_request = request.update_status(status_enum, message) + # Cache the latest ProviderFulfilment snapshot so DTO callers can surface it. + if provider_metadata: + fulfilment = provider_metadata.get("provider_fulfilment") + if fulfilment is not None: + updated_request = updated_request.with_last_fulfilment( + dataclasses.asdict(fulfilment) + ) + # Save updated request with self.uow_factory.create_unit_of_work() as uow: uow.requests.save(updated_request) @@ -178,10 +219,9 @@ def map_machine_status_to_result(self, status: str, request_type: RequestType) - return "executing" else: return "fail" + elif status == "running": + return "succeed" + elif status in ["pending", "launching"]: + return "executing" else: - if status == "running": - return "succeed" - elif status in ["pending", "launching"]: - return "executing" - else: - return "fail" + return "fail" diff --git a/src/orb/application/services/template_defaults_service.py b/src/orb/application/services/template_defaults_service.py index fb43ae2e5..2895457fe 100644 --- a/src/orb/application/services/template_defaults_service.py +++ b/src/orb/application/services/template_defaults_service.py @@ -5,10 +5,13 @@ from orb.domain.base.dependency_injection import injectable from orb.domain.base.ports.configuration_port import ConfigurationPort from orb.domain.base.ports.logging_port import LoggingPort +from orb.domain.base.ports.provider_registry_port import ProviderRegistryPort from orb.domain.base.utils import extract_provider_type -from orb.domain.template.extensions import TemplateExtensionRegistry from orb.domain.template.factory import TemplateFactoryPort from orb.domain.template.ports.template_defaults_port import TemplateDefaultsPort +from orb.domain.template.ports.template_extension_registry_port import ( + TemplateExtensionRegistryPort, +) from orb.domain.template.template_aggregate import Template @@ -32,7 +35,8 @@ def __init__( config_manager: ConfigurationPort, logger: LoggingPort, template_factory: Optional[TemplateFactoryPort] = None, - extension_registry: Optional[TemplateExtensionRegistry] = None, + extension_registry: Optional[TemplateExtensionRegistryPort] = None, + provider_registry: Optional[ProviderRegistryPort] = None, ) -> None: """ Initialize the template defaults service. @@ -41,12 +45,14 @@ def __init__( config_manager: Configuration port for accessing defaults logger: Logger for debugging and monitoring template_factory: Factory for creating domain templates - extension_registry: Registry for provider extensions + extension_registry: Registry port for provider extension defaults + provider_registry: Registry for resolving provider-contributed defaults """ self.config_manager = config_manager self.logger = logger self.template_factory = template_factory - self.extension_registry = extension_registry or TemplateExtensionRegistry + self.extension_registry = extension_registry + self.provider_registry = provider_registry def resolve_template_defaults( self, @@ -100,7 +106,13 @@ def resolve_template_defaults( ) # 4. Apply template values (highest priority - only for missing fields) - if template_dict.get("launch_template_id"): + # launch_template_id may be at top level (legacy) or inside provider_config (new path). + _pc = template_dict.get("provider_config") or {} + _has_lt = bool( + template_dict.get("launch_template_id") + or (_pc.get("launch_template_id") if isinstance(_pc, dict) else None) + ) + if _has_lt: lt_fields = [ k for k in ( @@ -284,18 +296,13 @@ def _get_provider_type_defaults(self, provider_type: str) -> dict[str, Any]: provider_defaults = provider_config.provider_defaults.get(provider_type) # type: ignore[union-attr] if provider_defaults and hasattr(provider_defaults, "template_defaults"): result = provider_defaults.template_defaults or {} - # Fallback: if no provider_api in template_defaults, read from - # handlers.defaults.default_handler (e.g. the configured default handler name) - if not result.get("provider_api") and hasattr(provider_defaults, "handlers"): - default_handler = ( - provider_defaults.handlers.defaults.default_handler - if provider_defaults.handlers - and hasattr(provider_defaults.handlers, "defaults") - else None - ) - if default_handler: + # Fallback: if no provider_api in template_defaults, delegate to the + # provider registry which reads it from the provider's registration. + if not result.get("provider_api") and self.provider_registry is not None: + default_api = self.provider_registry.get_default_api(provider_type) + if default_api: result = dict(result) - result["provider_api"] = default_handler + result["provider_api"] = default_api return result return {} @@ -464,16 +471,14 @@ def _get_extension_defaults( """ extension_defaults = {} + if self.extension_registry is None: + return extension_defaults + try: - # 1. Get provider type extension defaults - if self.extension_registry.has_extension(provider_type): - type_extension_defaults = self.extension_registry.get_extension_defaults( - provider_type - ) - extension_defaults.update(type_extension_defaults) - self.logger.debug( - "Applied %s type extension defaults", len(type_extension_defaults) - ) + # 1. Get provider type extension defaults — safe for unknown providers (returns {}) + type_extension_defaults = self.extension_registry.get_extension_defaults(provider_type) + extension_defaults.update(type_extension_defaults) + self.logger.debug("Applied %s type extension defaults", len(type_extension_defaults)) # 2. Get provider instance extension overrides if provider_instance_name: @@ -514,14 +519,15 @@ def _get_provider_instance_extension_defaults( and hasattr(provider, "extensions") and provider.extensions ): - # Use extension registry to process the extensions - if self.extension_registry.has_extension(provider_type): - return self.extension_registry.get_extension_defaults( + if self.extension_registry is not None: + # Delegate to extension registry — returns {} for unknown providers, + # so this is safe to call unconditionally. + result = self.extension_registry.get_extension_defaults( provider_type, provider.extensions ) - else: - # Return raw extensions if no registry entry - return provider.extensions + # Fall back to raw extensions dict when registry has no entry. + return result if result else provider.extensions + return provider.extensions return {} @@ -594,18 +600,6 @@ def validate_template_with_extensions( validation_result["warnings"].append(f"Domain validation failed: {e}") validation_result["domain_validation"] = "failed" - # Check for provider-specific validation - provider_type = ( - self._get_provider_type(provider_instance_name) if provider_instance_name else None - ) - if provider_type and hasattr(template, f"validate_{provider_type}"): - try: - getattr(template, f"validate_{provider_type}")() - validation_result[f"{provider_type}_validation"] = "passed" - except Exception as e: - validation_result["warnings"].append(f"{provider_type} validation failed: {e}") - validation_result[f"{provider_type}_validation"] = "failed" - except Exception as e: validation_result["errors"].append(f"Template creation with extensions failed: {e}") validation_result["is_valid"] = False diff --git a/src/orb/application/template/commands.py b/src/orb/application/template/commands.py index 3785fb782..de4114fcd 100644 --- a/src/orb/application/template/commands.py +++ b/src/orb/application/template/commands.py @@ -16,7 +16,7 @@ class CreateTemplateCommand(BaseCommand): template_id: str name: Optional[str] = None description: Optional[str] = None - provider_api: str + provider_api: Optional[str] = None instance_type: Optional[str] = None image_id: str tags: dict[str, str] = Field(default_factory=dict) diff --git a/src/orb/bootstrap/infrastructure_services.py b/src/orb/bootstrap/infrastructure_services.py index 6a2c8ea12..3fbbd48e0 100644 --- a/src/orb/bootstrap/infrastructure_services.py +++ b/src/orb/bootstrap/infrastructure_services.py @@ -35,10 +35,14 @@ def create_template_defaults_service(c): from orb.application.services.template_defaults_service import ( TemplateDefaultsService, ) + from orb.infrastructure.registry.template_extension_registry import ( + TemplateExtensionRegistryAdapter, + ) return TemplateDefaultsService( config_manager=c.get(ConfigurationPort), logger=c.get(LoggingPort), + extension_registry=TemplateExtensionRegistryAdapter(), ) from orb.domain.template.ports.template_defaults_port import TemplateDefaultsPort @@ -73,18 +77,30 @@ def create_template_generation_service(c): # Register TemplateFactory as a singleton so handlers can receive it via DI def create_template_factory(c: DIContainer): + import importlib + import importlib.util + from orb.domain.template.factory import TemplateFactory + from orb.providers.registration import _REGISTERED_PROVIDERS factory = TemplateFactory(logger=c.get(LoggingPort)) - try: - from orb.providers.aws.registration import register_aws_template_factory - - register_aws_template_factory(factory, c.get(LoggingPort)) - except ImportError as exc: - c.get(LoggingPort).debug( - "AWS provider module not available; AWS-specific templates will not be registered: %s", - exc, - ) + logger_port = c.get(LoggingPort) + for _name in _REGISTERED_PROVIDERS: + _mod_path = f"orb.providers.{_name}.registration" + if importlib.util.find_spec(_mod_path) is None: + continue + try: + _mod = importlib.import_module(_mod_path) + _reg_fn = getattr(_mod, f"register_{_name}_template_factory", None) + if _reg_fn is not None: + _reg_fn(factory, logger_port) + except ImportError as exc: + logger_port.debug( + "%s provider module not available; provider-specific templates will not be" + " registered: %s", + _name, + exc, + ) return factory from orb.domain.template.factory import TemplateFactory, TemplateFactoryPort diff --git a/src/orb/bootstrap/provider_services.py b/src/orb/bootstrap/provider_services.py index c1820f159..a8280459f 100644 --- a/src/orb/bootstrap/provider_services.py +++ b/src/orb/bootstrap/provider_services.py @@ -62,26 +62,41 @@ def create_machine_sync_service(c): def _register_provider_utility_services(container: DIContainer) -> None: - """Register provider-specific utility services only (not provider instances).""" + """Register provider-specific utility services for all registered providers.""" + import importlib + import importlib.util + + from orb.providers.registration import _REGISTERED_PROVIDERS + logger = get_logger(__name__) - # Register AWS utility services if available - try: - import importlib.util - - # Check if AWS provider is available - if importlib.util.find_spec("orb.providers.aws"): - try: - from orb.providers.aws.registration import register_aws_services_with_di - - register_aws_services_with_di(container) - logger.debug("AWS utility services registered with DI") - except Exception as e: - logger.warning("Failed to register AWS utility services: %s", str(e)) - - else: - logger.debug("AWS provider not available, skipping AWS utility service registration") - except ImportError: - logger.debug("AWS provider not available, skipping AWS utility service registration") - except Exception as e: - logger.warning("Failed to register AWS utility services: %s", str(e)) + for name in _REGISTERED_PROVIDERS: + mod_path = f"orb.providers.{name}.registration" + try: + spec = importlib.util.find_spec(mod_path) + except (ImportError, ValueError) as e: + logger.debug("%s find_spec raised %s; skipping", name, type(e).__name__) + continue + if spec is None: + logger.debug("%s provider not available, skipping utility service registration", name) + continue + try: + mod = importlib.import_module(mod_path) + + # Register DI utility services (e.g. AWS template adapter, clients) + di_fn = getattr(mod, f"register_{name}_services_with_di", None) + if di_fn is not None: + di_fn(container) + logger.debug("%s utility services registered with DI", name) + + # Register auth strategies so the auth registry can resolve them + # without server.py importing provider classes directly. + # Pass None for the logging port; registration is best-effort and + # the bootstrap logger (ContextLogger) does not implement LoggingPort. + auth_fn = getattr(mod, f"register_{name}_auth_strategies", None) + if auth_fn is not None: + auth_fn(None) + logger.debug("%s auth strategies registered", name) + + except Exception as e: + logger.warning("Failed to register %s utility services: %s", name, str(e)) diff --git a/src/orb/cli/args.py b/src/orb/cli/args.py index f21729560..bd09740a8 100644 --- a/src/orb/cli/args.py +++ b/src/orb/cli/args.py @@ -14,6 +14,7 @@ from orb.domain.machine.machine_status import MachineStatus from orb.domain.request.value_objects import RequestStatus +from orb.infrastructure.registry.cli_spec_registry import CLISpecRegistry try: import rich.console as _rich_console # type: ignore[import-untyped,import-not-found] @@ -302,8 +303,8 @@ def add_provider_actions(subparsers): providers_add.add_argument( "--provider-type", dest="provider_type", required=True, help="Provider type (e.g. aws)" ) - providers_add.add_argument("--aws-profile", help="AWS profile name") - providers_add.add_argument("--aws-region", help="AWS region") + for _spec in CLISpecRegistry.all().values(): + _spec.add_arguments(providers_add) providers_add.add_argument("--name", help="Provider instance name") providers_add.add_argument("--discover", action="store_true", help="Discover infrastructure") @@ -314,8 +315,8 @@ def add_provider_actions(subparsers): providers_update = subparsers.add_parser("update", help="Update provider configuration") add_global_arguments(providers_update) providers_update.add_argument("provider_name", help="Provider instance name") - providers_update.add_argument("--aws-region", help="Update region") - providers_update.add_argument("--aws-profile", help="Update profile") + for _spec in CLISpecRegistry.all().values(): + _spec.add_arguments(providers_update) providers_set_default = subparsers.add_parser("set-default", help="Set default provider") add_global_arguments(providers_set_default) @@ -424,6 +425,13 @@ def build_parser() -> tuple[argparse.ArgumentParser, dict]: Returns: tuple: (parser, resource_parsers_dict) """ + # Ensure CLI specs are registered before iterating the registry. This is + # a lightweight bootstrap (no full provider initialisation) so it is safe + # to call before any application context exists. + from orb.providers.registration import register_all_provider_cli_specs + + register_all_provider_cli_specs() + from orb._package import DESCRIPTION, DOCS_URL parser = argparse.ArgumentParser( @@ -709,6 +717,10 @@ def build_parser() -> tuple[argparse.ArgumentParser, dict]: "--fleet-role", help="Spot Fleet IAM role ARN or name for template_defaults (non-interactive only)", ) + # Inject per-provider CLI flags (e.g. --aws-profile, --aws-region) so that + # _get_default_config can use spec.extract_config() in non-interactive mode. + for _spec in CLISpecRegistry.all().values(): + _spec.add_arguments(init_parser) return parser, resource_parsers diff --git a/src/orb/config/loader.py b/src/orb/config/loader.py index c35805b8d..39171cf4f 100644 --- a/src/orb/config/loader.py +++ b/src/orb/config/loader.py @@ -192,11 +192,28 @@ def _build_raw_config_from_dict( def _load_strategy_defaults(cls, config_manager=None) -> dict[str, Any]: merged: dict[str, Any] = {} try: - from orb.providers.registry import get_provider_registry - - registry = get_provider_registry() - registry.ensure_provider_type_registered("aws") - cls._merge_config(merged, registry.collect_defaults()) + from orb.providers.registry import DefaultsLoaderRegistry + + # Ensure at least the built-in loaders are registered even when + # _load_strategy_defaults is called before a full application + # bootstrap (e.g. bare config loading in tests or the CLI parser). + # This mirrors what the old pkgutil.iter_modules approach did + # implicitly — importing each provider's registration module + # triggered side-effect registrations. + if not DefaultsLoaderRegistry.registered_providers(): + from orb.providers.registration import register_all_defaults_loaders + + register_all_defaults_loaders() + + for provider_type, loader in DefaultsLoaderRegistry.all().items(): + try: + defaults = loader.load_defaults() + if defaults: + cls._merge_config(merged, defaults) + except Exception as e: + get_config_logger().warning( + "Failed to load defaults from provider '%s': %s", provider_type, e + ) except Exception as e: get_config_logger().warning("Failed to load provider defaults: %s", e) try: diff --git a/src/orb/config/schemas/cleanup_schema.py b/src/orb/config/schemas/cleanup_schema.py index b01e2be70..fcb108274 100644 --- a/src/orb/config/schemas/cleanup_schema.py +++ b/src/orb/config/schemas/cleanup_schema.py @@ -1,8 +1,17 @@ -"""Re-export shim — CleanupConfig has moved to orb.providers.aws.configuration.cleanup_config.""" +"""Re-export shim — CleanupConfig has moved to orb.providers.aws.configuration.cleanup_config. -from orb.providers.aws.configuration.cleanup_config import ( - CleanupConfig, - CleanupResourcesConfig, -) +Note: This shim imports from the AWS provider package. When the [aws] extra is not +installed, CleanupConfig and CleanupResourcesConfig are set to None. Phase 2 of the +provider-extras migration will invert this dependency so the models live in core. +""" + +try: + from orb.providers.aws.configuration.cleanup_config import ( + CleanupConfig, + CleanupResourcesConfig, + ) +except ImportError: + CleanupConfig = None # type: ignore[assignment,misc] + CleanupResourcesConfig = None # type: ignore[assignment,misc] __all__ = ["CleanupConfig", "CleanupResourcesConfig"] diff --git a/src/orb/config/schemas/server_schema.py b/src/orb/config/schemas/server_schema.py index 0601e07b9..dad24eea5 100644 --- a/src/orb/config/schemas/server_schema.py +++ b/src/orb/config/schemas/server_schema.py @@ -2,30 +2,105 @@ from typing import Any, Optional -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator + +_ALLOWED_JWT_ALGORITHMS = frozenset({"HS256", "HS384", "HS512"}) + + +class BearerTokenAuthSubConfig(BaseModel): + """Typed sub-configuration for the bearer_token auth strategy.""" + + model_config = ConfigDict(extra="forbid") + + secret_key: str = Field(..., description="Secret key for JWT signing/verification (>=32 bytes)") + algorithm: str = Field("HS256", description="JWT algorithm (HS256, HS384, or HS512)") + token_expiry: int = Field(3600, description="Token expiry in seconds") + + @field_validator("algorithm") + @classmethod + def _validate_algorithm(cls, value: str) -> str: + """Restrict to HMAC algorithms; explicitly reject 'none' and unknown values.""" + if value.lower() == "none": + raise ValueError( + "Algorithm 'none' is not permitted — it disables signature verification." + ) + if value not in _ALLOWED_JWT_ALGORITHMS: + raise ValueError( + f"Unsupported JWT algorithm {value!r}. " + f"Allowed values: {sorted(_ALLOWED_JWT_ALGORITHMS)}" + ) + return value + + +class IAMAuthSubConfig(BaseModel): + """Typed sub-configuration for AWS IAM auth strategy. + + **Security note — assume_permissions:** + Setting ``assume_permissions=True`` bypasses real AWS IAM evaluation and grants + every action in ``required_actions`` to any authenticated principal. This is a + deliberate development/testing escape hatch and MUST NOT be enabled in production. + The IAMAuthStrategy enforces this by requiring the environment variable + ``ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY=true`` to be set alongside the config flag; + without it the flag is ignored and permissions are denied by default. + """ + + model_config = ConfigDict(extra="forbid") + + region: str = Field("us-east-1", description="AWS region") + profile: Optional[str] = Field(None, description="AWS profile") + required_actions: list[str] = Field(default_factory=list, description="Required IAM actions") + assume_permissions: bool = Field( + False, + description=( + "DEV/TEST ONLY — grant all required_actions without AWS evaluation. " + "Has no effect unless ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY=true is also set." + ), + ) + + +class CognitoAuthSubConfig(BaseModel): + """Typed sub-configuration for AWS Cognito auth strategy.""" + + model_config = ConfigDict(extra="forbid") + + user_pool_id: str = Field("", description="Cognito User Pool ID") + client_id: str = Field("", description="Cognito App Client ID") + region: str = Field("us-east-1", description="AWS region") + jwks_url: Optional[str] = Field(None, description="JWKS URL (auto-generated if omitted)") + + +class ProviderAuthSubConfig(BaseModel): + """Typed sub-configuration for provider-specific auth strategies.""" + + model_config = ConfigDict(extra="forbid") + + iam: Optional[IAMAuthSubConfig] = Field(None, description="IAM auth sub-configuration") + cognito: Optional[CognitoAuthSubConfig] = Field( + None, description="Cognito auth sub-configuration" + ) class AuthConfig(BaseModel): """Authentication configuration.""" - model_config = ConfigDict(extra="allow") # Allow provider-specific auth configs + model_config = ConfigDict(extra="forbid") enabled: bool = Field(False, description="Enable authentication") strategy: str = Field( "none", - description="Authentication strategy (none, bearer_token, iam, cognito, oauth)", + description="Authentication strategy (none, bearer_token, bearer_token_enhanced, iam, cognito)", ) # Bearer token configuration - bearer_token: Optional[dict[str, Any]] = Field( + bearer_token: Optional[BearerTokenAuthSubConfig] = Field( None, description="Bearer token strategy configuration" ) - # OAuth configuration + # OAuth configuration (kept as untyped dict for forward compatibility) oauth: Optional[dict[str, Any]] = Field(None, description="OAuth strategy configuration") # Provider-specific auth configurations - provider_auth: Optional[dict[str, Any]] = Field( + provider_auth: Optional[ProviderAuthSubConfig] = Field( None, description="Provider-specific auth configuration" ) diff --git a/src/orb/domain/base/events/provider_events.py b/src/orb/domain/base/events/provider_events.py index 701527d95..53339bfb5 100644 --- a/src/orb/domain/base/events/provider_events.py +++ b/src/orb/domain/base/events/provider_events.py @@ -6,13 +6,13 @@ from pydantic import Field from orb.domain.base.events.base_events import DomainEvent -from orb.domain.base.provider_interfaces import ProviderInstanceState, ProviderType +from orb.domain.base.provider_interfaces import ProviderInstanceState class ProviderOperationEvent(DomainEvent): """Event raised for provider operations.""" - provider_type: ProviderType + provider_type: str # e.g. "aws" operation_type: str # e.g., "create_instance", "terminate_instance" provider_resource_type: str # e.g., "instance", "volume" provider_resource_id: Optional[str] = Field(default=None) @@ -23,7 +23,7 @@ def model_post_init(self, __context: Any) -> None: """Initialize aggregate information after model creation.""" # Set the base class fields object.__setattr__(self, "aggregate_id", self.provider_resource_id or str(uuid4())) - object.__setattr__(self, "aggregate_type", f"{self.provider_type.value}_resource") + object.__setattr__(self, "aggregate_type", f"{self.provider_type}_resource") super().model_post_init(__context) if not self.operation_type: raise ValueError("Operation type cannot be empty") @@ -34,15 +34,15 @@ def model_post_init(self, __context: Any) -> None: class ProviderRateLimitEvent(DomainEvent): """Event raised when provider rate limiting occurs.""" - provider_type: ProviderType + provider_type: str # e.g. "aws" service_name: str operation_name: str retry_after: Optional[int] = Field(default=None) # seconds def model_post_init(self, __context: Any) -> None: """Initialize aggregate information after model creation.""" - object.__setattr__(self, "aggregate_id", f"{self.provider_type.value}_{self.service_name}") - object.__setattr__(self, "aggregate_type", f"{self.provider_type.value}_service") + object.__setattr__(self, "aggregate_id", f"{self.provider_type}_{self.service_name}") + object.__setattr__(self, "aggregate_type", f"{self.provider_type}_service") super().model_post_init(__context) if not self.service_name: raise ValueError("Service name cannot be empty") @@ -53,7 +53,7 @@ def model_post_init(self, __context: Any) -> None: class ProviderCredentialsEvent(DomainEvent): """Event raised for provider credentials operations.""" - provider_type: ProviderType + provider_type: str # e.g. "aws" credential_type: str # e.g., "access_key", "service_account", "managed_identity" operation: str # e.g., "refresh", "validate", "expire" status: str # success, failure, warning @@ -61,8 +61,8 @@ class ProviderCredentialsEvent(DomainEvent): def model_post_init(self, __context: Any) -> None: """Initialize aggregate information after model creation.""" - object.__setattr__(self, "aggregate_id", f"{self.provider_type.value}_credentials") - object.__setattr__(self, "aggregate_type", f"{self.provider_type.value}_auth") + object.__setattr__(self, "aggregate_id", f"{self.provider_type}_credentials") + object.__setattr__(self, "aggregate_type", f"{self.provider_type}_auth") super().model_post_init(__context) if not self.credential_type: raise ValueError("Credential type cannot be empty") @@ -75,7 +75,7 @@ def model_post_init(self, __context: Any) -> None: class ProviderResourceStateChangedEvent(DomainEvent): """Event raised when provider resource state changes.""" - provider_type: ProviderType + provider_type: str # e.g. "aws" resource_type: str resource_id: str previous_state: Optional[ProviderInstanceState] = Field(default=None) @@ -85,9 +85,7 @@ class ProviderResourceStateChangedEvent(DomainEvent): def model_post_init(self, __context: Any) -> None: """Initialize aggregate information after model creation.""" object.__setattr__(self, "aggregate_id", self.resource_id) - object.__setattr__( - self, "aggregate_type", f"{self.provider_type.value}_{self.resource_type}" - ) + object.__setattr__(self, "aggregate_type", f"{self.provider_type}_{self.resource_type}") super().model_post_init(__context) if not self.resource_type: raise ValueError("Resource type cannot be empty") @@ -98,7 +96,7 @@ def model_post_init(self, __context: Any) -> None: class ProviderConfigurationEvent(DomainEvent): """Event raised for provider configuration changes.""" - provider_type: ProviderType + provider_type: str # e.g. "aws" configuration_type: str # e.g., "region", "endpoint", "timeout" old_value: Optional[str] = Field(default=None) new_value: Optional[str] = Field(default=None) @@ -106,8 +104,8 @@ class ProviderConfigurationEvent(DomainEvent): def model_post_init(self, __context: Any) -> None: """Initialize aggregate information after model creation.""" - object.__setattr__(self, "aggregate_id", f"{self.provider_type.value}_config") - object.__setattr__(self, "aggregate_type", f"{self.provider_type.value}_configuration") + object.__setattr__(self, "aggregate_id", f"{self.provider_type}_config") + object.__setattr__(self, "aggregate_type", f"{self.provider_type}_configuration") super().model_post_init(__context) if not self.configuration_type: raise ValueError("Configuration type cannot be empty") @@ -116,7 +114,7 @@ def model_post_init(self, __context: Any) -> None: class ProviderHealthCheckEvent(DomainEvent): """Event raised for provider health checks.""" - provider_type: ProviderType + provider_type: str # e.g. "aws" service_name: str health_status: str # healthy, unhealthy, degraded response_time_ms: Optional[int] = Field(default=None) @@ -124,8 +122,8 @@ class ProviderHealthCheckEvent(DomainEvent): def model_post_init(self, __context: Any) -> None: """Initialize aggregate information after model creation.""" - object.__setattr__(self, "aggregate_id", f"{self.provider_type.value}_{self.service_name}") - object.__setattr__(self, "aggregate_type", f"{self.provider_type.value}_health") + object.__setattr__(self, "aggregate_id", f"{self.provider_type}_{self.service_name}") + object.__setattr__(self, "aggregate_type", f"{self.provider_type}_health") super().model_post_init(__context) if not self.service_name: raise ValueError("Service name cannot be empty") diff --git a/src/orb/domain/base/exceptions.py b/src/orb/domain/base/exceptions.py index b0d9b6278..97bda6662 100644 --- a/src/orb/domain/base/exceptions.py +++ b/src/orb/domain/base/exceptions.py @@ -92,3 +92,13 @@ class QuotaError(DomainException): class QuotaExceededError(QuotaError): """Raised when a specific provider quota is exceeded.""" + + +class ProviderContractError(DomainException): + """Raised when a provider violates the ProviderFulfilment contract. + + Every call to ``check_hosts_status`` must return a ``CheckHostsStatusResult`` + that includes a ``ProviderFulfilment``. If the provider omits it the + application layer raises this error instead of silently falling back to + count-based logic, which is always wrong for weighted fleets. + """ diff --git a/src/orb/domain/base/follow_up_context.py b/src/orb/domain/base/follow_up_context.py new file mode 100644 index 000000000..7c4c16b9f --- /dev/null +++ b/src/orb/domain/base/follow_up_context.py @@ -0,0 +1,73 @@ +"""Typed follow-up context for provider operations that require background tracking. + +``FollowUpContext`` describes *what* kind of follow-up is needed after a provider +operation returns ``RequiresFollowUp``. These are pure value objects — frozen +dataclasses with literal discriminants and primitive fields — and therefore +belong in the domain layer alongside ``OperationOutcome``. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime +from typing import Literal + +# --------------------------------------------------------------------------- +# Individual context variants +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class TerminationFollowUpContext: + """Context for a return-machines follow-up. + + The provider accepted the termination request but instances are still in + ``shutting-down`` state. The background poller should wait until all + ``pending_instance_ids`` reach ``terminated`` before closing the request. + + Attributes: + follow_up_kind: Discriminator tag (always ``"termination"``). + pending_instance_ids: Instance IDs still being terminated. + expected_terminal_state: State that signals completion (``"terminated"`` + for EC2, ``"deleted"`` for other providers). + poll_after: Earliest wall-clock time to issue the next status check. + provider_handle: Optional provider-side tracking ID (e.g. a batch job + reference); may be ``None`` for simple TerminateInstances calls. + """ + + follow_up_kind: Literal["termination"] = field(default="termination", init=False) + pending_instance_ids: list[str] = field(default_factory=list) + expected_terminal_state: str = "terminated" + poll_after: datetime | None = None + provider_handle: str | None = None + + +@dataclass(frozen=True) +class DeploymentPollingFollowUpContext: + """Context for an acquire follow-up where the fleet is still initialising. + + The provider accepted the launch request. The background poller should + check the fleet/spot-request until all instances are ``running`` or a + terminal failure state is reached. + + Attributes: + follow_up_kind: Discriminator tag (always ``"deployment_polling"``). + pending_resource_ids: Fleet/request IDs still being fulfilled. + expected_terminal_state: State that signals successful completion + (default ``"running"``). + poll_after: Earliest wall-clock time to issue the next status check. + provider_handle: Provider-side tracking ID (fleet request ID, etc.). + """ + + follow_up_kind: Literal["deployment_polling"] = field(default="deployment_polling", init=False) + pending_resource_ids: list[str] = field(default_factory=list) + expected_terminal_state: str = "running" + poll_after: datetime | None = None + provider_handle: str | None = None + + +# --------------------------------------------------------------------------- +# Union type +# --------------------------------------------------------------------------- + +FollowUpContext = TerminationFollowUpContext | DeploymentPollingFollowUpContext diff --git a/src/orb/domain/base/operation_outcome.py b/src/orb/domain/base/operation_outcome.py new file mode 100644 index 000000000..495e2b858 --- /dev/null +++ b/src/orb/domain/base/operation_outcome.py @@ -0,0 +1,106 @@ +"""Typed discriminated union for provider operation outcomes. + +This module defines the ``OperationOutcome`` type — a pure domain concept that +replaces stringly-typed ``provider_metadata`` keys with explicit, exhaustively- +matchable variants. All variants are frozen dataclasses so they are +value-comparable and safe to store in caches or queues. + +Usage example (exhaustive match with ``assert_never``):: + + from typing import assert_never + from orb.domain.base.operation_outcome import ( + Accepted, Completed, Failed, OperationOutcome, RequiresFollowUp, + ) + + def handle(outcome: OperationOutcome) -> str: + match outcome: + case Accepted(request_id=rid): + return f"accepted, tracking {rid}" + case Completed(resource_ids=ids): + return f"done: {ids}" + case RequiresFollowUp(context=ctx): + return f"follow-up needed: {ctx.follow_up_kind}" + case Failed(error=msg): + return f"failed: {msg}" + case _ as unreachable: + assert_never(unreachable) +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from orb.domain.base.follow_up_context import FollowUpContext + + +@dataclass(frozen=True) +class Accepted: + """The provider accepted the request and is processing it asynchronously. + + This is the normal AWS outcome for ``acquire`` and ``return_machines``: + the API call succeeded but instances are ``pending`` / ``shutting-down``. + Callers must poll via ``get_status`` until a terminal outcome is reached. + + Attributes: + request_id: Provider-side tracking identifier (e.g. EC2Fleet request ID). + pending_resource_ids: Resource/instance IDs in a non-terminal state. + metadata: Optional provider-specific supplementary data. + """ + + request_id: str + pending_resource_ids: list[str] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class Completed: + """The operation reached a terminal successful state. + + All requested resources exist and are in their expected final state + (``running`` for acquire, ``terminated`` for return). + + Attributes: + resource_ids: IDs of the resources in their terminal state. + metadata: Optional provider-specific supplementary data. + """ + + resource_ids: list[str] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class RequiresFollowUp: + """The provider acknowledged the request but a background follow-up is needed. + + Used when the operation cannot be completed in a single synchronous call and + requires a specific follow-up action beyond simple polling (e.g. a webhook + callback, a secondary API call, or a domain-side state machine transition). + + Attributes: + context: Typed descriptor of *what* follow-up is required and *how*. + metadata: Optional provider-specific supplementary data. + """ + + context: FollowUpContext + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class Failed: + """The operation failed either permanently or transiently. + + Attributes: + error: Human-readable error description. + recoverable: ``True`` if a retry may succeed (e.g. throttle); ``False`` + for hard failures (e.g. invalid configuration). + metadata: Optional provider-specific supplementary data. + """ + + error: str + recoverable: bool = False + metadata: dict[str, Any] = field(default_factory=dict) + + +# Discriminated union — use ``match`` / ``isinstance`` for exhaustive dispatch. +OperationOutcome = Accepted | Completed | RequiresFollowUp | Failed diff --git a/src/orb/domain/base/ports/provider_cli_spec_port.py b/src/orb/domain/base/ports/provider_cli_spec_port.py index b6865afcf..67db2bc3e 100644 --- a/src/orb/domain/base/ports/provider_cli_spec_port.py +++ b/src/orb/domain/base/ports/provider_cli_spec_port.py @@ -1,4 +1,4 @@ -"""Protocol and registry for provider CLI argument specifications.""" +"""Protocol defining how a provider exposes itself to the CLI.""" import argparse from typing import Any, Protocol, runtime_checkable @@ -31,24 +31,3 @@ def generate_name(self, args: argparse.Namespace) -> str: # type: ignore[return def format_display(self, config: dict[str, Any]) -> list[tuple[str, str]]: # type: ignore[return] """Return a list of (label, value) pairs for display.""" pass - - -class CLISpecRegistry: - """Simple registry mapping provider type strings to ProviderCLISpecPort instances.""" - - _specs: dict[str, ProviderCLISpecPort] = {} - - @classmethod - def register(cls, provider_type: str, spec: ProviderCLISpecPort) -> None: - """Register a CLI spec for a provider type.""" - cls._specs[provider_type] = spec - - @classmethod - def get(cls, provider_type: str) -> ProviderCLISpecPort | None: - """Return the CLI spec for a provider type, or None if not registered.""" - return cls._specs.get(provider_type) - - @classmethod - def all(cls) -> dict[str, ProviderCLISpecPort]: - """Return all registered CLI specs.""" - return dict(cls._specs) diff --git a/src/orb/domain/base/ports/provider_defaults_loader_port.py b/src/orb/domain/base/ports/provider_defaults_loader_port.py new file mode 100644 index 000000000..885add913 --- /dev/null +++ b/src/orb/domain/base/ports/provider_defaults_loader_port.py @@ -0,0 +1,26 @@ +"""Protocol for per-provider defaults loaders.""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class ProviderDefaultsLoaderPort(Protocol): + """Contract for a loader that returns provider-contributed config defaults. + + Implementations read a provider's bundled defaults file (or any other + source) and return a raw config dict in the same shape as + ``default_config.json``. The loader is registered once per provider type + in ``DefaultsLoaderRegistry`` so that ``ConfigurationLoader`` can iterate + all registered loaders without knowing concrete provider classes. + """ + + def load_defaults(self) -> dict: # type: ignore[return] + """Return the provider's contributed config defaults. + + Returns: + Raw configuration dictionary (same shape as ``default_config.json``). + Return an empty dict if the provider has no defaults to contribute. + """ + ... diff --git a/src/orb/domain/base/ports/provider_registry_port.py b/src/orb/domain/base/ports/provider_registry_port.py index 459aac307..21d13b2d6 100644 --- a/src/orb/domain/base/ports/provider_registry_port.py +++ b/src/orb/domain/base/ports/provider_registry_port.py @@ -83,3 +83,11 @@ def create_strategy_by_type(self, provider_type: str, config: Any = None) -> Any def create_validator(self, provider_type: str) -> Optional[Any]: """Create a template validator using the registered factory for the given provider type.""" pass # type: ignore[return] + + @abstractmethod + def get_default_api(self, provider_type: str) -> Optional[str]: + """Return the default API name contributed by the given provider type's registration. + + Returns None if the provider type is not registered or has no default API. + """ + pass # type: ignore[return] diff --git a/src/orb/domain/base/provider_fulfilment.py b/src/orb/domain/base/provider_fulfilment.py new file mode 100644 index 000000000..2063fe7bd --- /dev/null +++ b/src/orb/domain/base/provider_fulfilment.py @@ -0,0 +1,70 @@ +"""Provider fulfilment contract — domain value objects. + +Every call to ``check_hosts_status`` on a provider handler MUST return a +``CheckHostsStatusResult`` that includes a ``ProviderFulfilment``. The +application layer trusts this verdict exclusively — no count math, no +provider-specific key inspection. + +See design doc: .claude/plans/PLAN-provider-fulfilment-contract.md +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +# The four possible states a provider can report for a request. +# +# fulfilled — target capacity met, all instances running, no failures. +# in_progress — still provisioning; poll again. +# partial — some capacity met but the provider has given up on the rest +# (e.g. instant fleet returned fewer instances than requested). +# failed — provider could not fulfil the request at all. +FulfilmentState = Literal["fulfilled", "in_progress", "partial", "failed"] + + +@dataclass(frozen=True) +class ProviderFulfilment: + """Provider-computed verdict on whether a request is fulfilled. + + Each provider knows its own API semantics (capacity units, instance + count, weighted vCPU, native fulfilment state machines). This object + is the single contract the application layer trusts — no count math + or provider-specific keys in shared services. + + Attributes: + state: High-level verdict (see FulfilmentState). + message: Human-readable summary for status display. + target_units: Capacity units requested (may differ from instance count + for weighted fleets/ASGs). None for providers without the concept. + fulfilled_units: Capacity units currently fulfilled. None if unknown. + running_count: Number of instances in the running state. None if unknown. + pending_count: Number of instances still starting/pending. None if unknown. + failed_count: Number of instances in a failure state. None if unknown. + """ + + state: FulfilmentState + message: str + target_units: int | None = None + fulfilled_units: int | None = None + running_count: int | None = None + pending_count: int | None = None + failed_count: int | None = None + + +@dataclass(frozen=True) +class CheckHostsStatusResult: + """Combined result of a ``check_hosts_status`` handler call. + + Bundles the per-instance details list (existing API surface) with the + provider-computed fulfilment verdict so callers never need to re-derive + fulfilment from raw instance counts. + + Attributes: + instances: List of per-instance detail dicts (same format as the + previous ``list[dict]`` return type from each handler). + fulfilment: Provider's verdict on whether the request is fulfilled. + """ + + instances: list[dict] + fulfilment: ProviderFulfilment diff --git a/src/orb/domain/base/provider_interfaces.py b/src/orb/domain/base/provider_interfaces.py index 42d5d04c4..4fce9b406 100644 --- a/src/orb/domain/base/provider_interfaces.py +++ b/src/orb/domain/base/provider_interfaces.py @@ -1,18 +1,10 @@ """Provider interfaces for domain layer - comprehensive provider abstraction.""" from dataclasses import dataclass -from enum import Enum +from enum import Enum # retained for ProviderInstanceState from typing import Optional, Protocol -class ProviderType(str, Enum): - """Supported provider types.""" - - AWS = "aws" - PROVIDER1 = "provider1" - Provider2 = "provider2" - - class ProviderInstanceState(str, Enum): """Provider-agnostic instance states.""" @@ -51,7 +43,7 @@ def __post_init__(self) -> None: class ProviderResourceIdentifier: """Provider-agnostic resource identifier.""" - provider_type: ProviderType + provider_type: str # e.g. "aws" resource_type: str # e.g., "instance", "volume", "network" identifier: str # Provider-specific ID region: Optional[str] = None @@ -109,8 +101,8 @@ class ProviderAdapter(Protocol): """Main provider adapter interface.""" @property - def provider_type(self) -> ProviderType: - """Get the provider type.""" + def provider_type(self) -> str: + """Get the provider type (e.g. ``"aws"``).""" ... @property @@ -140,10 +132,10 @@ def create_launch_template( class ProviderAdapterFactory(Protocol): """Factory for creating provider adapters.""" - def create_adapter(self, provider_type: ProviderType) -> ProviderAdapter: - """Create a provider adapter for the specified type.""" + def create_adapter(self, provider_type: str) -> ProviderAdapter: + """Create a provider adapter for the specified type (e.g. ``"aws"``).""" ... - def get_supported_providers(self) -> list[ProviderType]: - """Get list of supported provider types.""" + def get_supported_providers(self) -> list[str]: + """Get list of supported provider type names.""" ... diff --git a/src/orb/domain/request/aggregate.py b/src/orb/domain/request/aggregate.py index 0de145424..562060ad1 100644 --- a/src/orb/domain/request/aggregate.py +++ b/src/orb/domain/request/aggregate.py @@ -243,6 +243,27 @@ def update_metadata(self, updates: dict) -> "Request": fields["version"] = self.version + 1 return Request.model_validate(fields) + def with_last_fulfilment(self, fulfilment_dict: dict) -> "Request": + """Store a serialized ProviderFulfilment snapshot in metadata. + + The snapshot is written under the key ``last_fulfilment``. It is a + plain dict so it survives round-trips through any persistence layer. + The value is provider-agnostic — ProviderFulfilment is a domain value + object — so storing it here violates no domain boundary. + + Args: + fulfilment_dict: Result of ``ProviderFulfilment``-like ``__dict__`` + or ``dataclasses.asdict`` — a plain serialisable dict. + + Returns: + Updated Request with ``metadata["last_fulfilment"]`` set. + """ + new_metadata = {**self.metadata, "last_fulfilment": fulfilment_dict} + fields = self.model_dump() + fields["metadata"] = new_metadata + fields["version"] = self.version + 1 + return Request.model_validate(fields) + def with_launch_template_info(self, template_id: str, version: str) -> "Request": new_provider_data = { **self.provider_data, diff --git a/src/orb/domain/template/extensions.py b/src/orb/domain/template/extensions.py index 0fb5ef99b..d91d813d5 100644 --- a/src/orb/domain/template/extensions.py +++ b/src/orb/domain/template/extensions.py @@ -1,9 +1,7 @@ -"""Template extension registry for provider-specific extensions.""" +"""Abstract base for provider-specific template extensions.""" from abc import ABC, abstractmethod -from typing import Any, Optional - -from pydantic import BaseModel +from typing import Any class TemplateExtension(ABC): @@ -16,159 +14,3 @@ def to_template_defaults(self) -> dict[str, Any]: @abstractmethod def get_provider_type(self) -> str: """Get the provider type this extension supports.""" - - -class TemplateExtensionRegistry: - """Registry for provider-specific template extensions. - - This registry follows the Open/Closed Principle - it's open for extension - (new providers can be added) but closed for modification (existing code - doesn't need to change when new providers are added). - """ - - _extensions: dict[str, type[BaseModel]] = {} - _extension_instances: dict[str, TemplateExtension] = {} - - @classmethod - def register_extension(cls, provider_type: str, extension_class: type[BaseModel]) -> None: - """Register a provider-specific extension configuration class. - - Args: - provider_type: The provider type (e.g., 'aws', 'provider1', 'provider2') - extension_class: The Pydantic model class for the extension configuration - """ - if not issubclass(extension_class, BaseModel): - raise ValueError(f"Extension class must be a Pydantic BaseModel, got {extension_class}") - - cls._extensions[provider_type] = extension_class - - @classmethod - def register_extension_instance( - cls, provider_type: str, extension_instance: TemplateExtension - ) -> None: - """Register a provider-specific extension instance. - - Args: - provider_type: The provider type (e.g., 'aws', 'provider1', 'provider2') - extension_instance: The extension instance implementing TemplateExtension - """ - if not isinstance(extension_instance, TemplateExtension): - raise ValueError( - f"Extension instance must implement TemplateExtension, got {type(extension_instance)}" - ) - - cls._extension_instances[provider_type] = extension_instance - - @classmethod - def get_extension_class(cls, provider_type: str) -> Optional[type[BaseModel]]: - """Get extension configuration class for a provider. - - Args: - provider_type: The provider type to get extension class for - - Returns: - The extension configuration class or None if not registered - """ - return cls._extensions.get(provider_type) - - @classmethod - def get_extension_instance(cls, provider_type: str) -> Optional[TemplateExtension]: - """Get extension instance for a provider. - - Args: - provider_type: The provider type to get extension instance for - - Returns: - The extension instance or None if not registered - """ - return cls._extension_instances.get(provider_type) - - @classmethod - def has_extension(cls, provider_type: str) -> bool: - """Check if a provider has registered extensions. - - Args: - provider_type: The provider type to check - - Returns: - True if the provider has registered extensions - """ - return provider_type in cls._extensions or provider_type in cls._extension_instances - - @classmethod - def get_supported_providers(cls) -> list[str]: - """Get list of providers with registered extensions. - - Returns: - List of provider types that have registered extensions - """ - all_providers = set(cls._extensions.keys()) | set(cls._extension_instances.keys()) - return list(all_providers) - - @classmethod - def create_extension_config( - cls, provider_type: str, config_data: dict[str, Any] - ) -> Optional[BaseModel]: - """Create an extension configuration instance for a provider. - - Args: - provider_type: The provider type - config_data: The configuration data to create the extension with - - Returns: - The created extension configuration instance or None if provider not registered - """ - extension_class = cls.get_extension_class(provider_type) - if extension_class: - return extension_class(**config_data) - return None - - @classmethod - def get_extension_defaults( - cls, provider_type: str, config_data: Optional[dict[str, Any]] = None - ) -> dict[str, Any]: - """Get template defaults from provider extension. - - Args: - provider_type: The provider type - config_data: Optional configuration data to create extension with - - Returns: - Dictionary of template defaults from the extension - """ - # Try extension instance first - extension_instance = cls.get_extension_instance(provider_type) - if extension_instance: - return extension_instance.to_template_defaults() # type: ignore[attr-defined] - - # Try creating from extension class - if config_data: - extension_config = cls.create_extension_config(provider_type, config_data) - if extension_config and hasattr(extension_config, "to_template_defaults"): - return extension_config.to_template_defaults() # type: ignore[attr-defined] - - return {} - - @classmethod - def clear_registry(cls) -> None: - """Clear all registered extensions (mainly for testing).""" - cls._extensions.clear() - cls._extension_instances.clear() - - -# Convenience functions for common operations -def register_provider_extension(provider_type: str, extension_class: type[BaseModel]) -> None: - """Register a provider extension.""" - TemplateExtensionRegistry.register_extension(provider_type, extension_class) - - -def get_provider_extension_defaults( - provider_type: str, config_data: Optional[dict[str, Any]] = None -) -> dict[str, Any]: - """Get provider extension defaults.""" - return TemplateExtensionRegistry.get_extension_defaults(provider_type, config_data) - - -def has_provider_extension(provider_type: str) -> bool: - """Check if provider has extensions.""" - return TemplateExtensionRegistry.has_extension(provider_type) diff --git a/src/orb/domain/template/factory.py b/src/orb/domain/template/factory.py index 746bd6d2d..2837ee71b 100644 --- a/src/orb/domain/template/factory.py +++ b/src/orb/domain/template/factory.py @@ -4,7 +4,6 @@ from typing import Any, Optional, Protocol from orb.domain.base.ports.logging_port import LoggingPort -from orb.domain.template.extensions import TemplateExtensionRegistry from orb.domain.template.template_aggregate import Template @@ -49,16 +48,16 @@ class TemplateFactory(BaseTemplateFactory): def __init__( self, - extension_registry: Optional[TemplateExtensionRegistry] = None, + extension_registry: Optional[Any] = None, logger: Optional[LoggingPort] = None, ) -> None: """Initialize template factory. Args: - extension_registry: Registry for provider extensions (defaults to global registry) + extension_registry: Registry for provider extensions (injected by infrastructure layer) logger: Logger port for logging factory operations """ - self._extension_registry = extension_registry or TemplateExtensionRegistry + self._extension_registry = extension_registry self._logger = logger # Registry of provider-specific template classes @@ -192,7 +191,11 @@ def create_template_with_extensions( merged_data = template_data.copy() # Apply extension defaults from registry - if provider_type and self._extension_registry.has_extension(provider_type): + if ( + provider_type + and self._extension_registry is not None + and self._extension_registry.has_extension(provider_type) + ): extension_defaults = self._extension_registry.get_extension_defaults( provider_type, extension_data ) diff --git a/src/orb/domain/template/ports/template_extension_registry_port.py b/src/orb/domain/template/ports/template_extension_registry_port.py new file mode 100644 index 000000000..1f0b55cc2 --- /dev/null +++ b/src/orb/domain/template/ports/template_extension_registry_port.py @@ -0,0 +1,34 @@ +"""Port for provider template extension registries.""" + +from __future__ import annotations + +from typing import Any, Optional, Protocol, runtime_checkable + + +@runtime_checkable +class TemplateExtensionRegistryPort(Protocol): + """Contract for a registry that provides provider extension defaults. + + The application layer depends only on this protocol — not on the concrete + registry implementation — keeping the application→infrastructure boundary clean. + Implementations may delegate to class-level state (e.g. the global + ``TemplateExtensionRegistry``) via an adapter registered in DI. + """ + + def get_extension_defaults( + self, + provider_type: str, + config_data: Optional[dict[str, Any]] = None, + ) -> dict[str, Any]: # type: ignore[return] + """Return template defaults contributed by the named provider extension. + + Args: + provider_type: Provider type string (e.g. ``'aws'``). + config_data: Optional per-instance configuration data that may be + used to instantiate an extension class on demand. + + Returns: + Mapping of template field names to default values. Returns an + empty dict when no extension is registered for *provider_type*. + """ + ... diff --git a/src/orb/infrastructure/auth/registry.py b/src/orb/infrastructure/auth/registry.py index de9823d12..ef4f76858 100644 --- a/src/orb/infrastructure/auth/registry.py +++ b/src/orb/infrastructure/auth/registry.py @@ -35,7 +35,8 @@ def register_strategy( Args: strategy_name: Name of the strategy (e.g., 'none', 'bearer_token', 'oauth') - strategy_factory: Factory function that creates the strategy instance + strategy_factory: Factory callable that creates the strategy instance. + Must expose a ``from_auth_config(auth_config)`` classmethod. """ # Create a simple config factory that passes through kwargs @@ -44,13 +45,16 @@ def config_factory(**kwargs): self.register_type(strategy_name, strategy_factory, config_factory) - def get_strategy(self, strategy_name: str, **kwargs) -> AuthPort: + def get_strategy(self, strategy_name: str, auth_config: Any) -> AuthPort: """ - Get an authentication strategy instance. + Get an authentication strategy instance built from *auth_config*. + + Internally delegates to ``strategy_factory.from_auth_config(auth_config)`` + so that each strategy class owns its own config-extraction logic. Args: - strategy_name: Name of the strategy - **kwargs: Arguments to pass to the strategy factory + strategy_name: Name of the registered strategy + auth_config: AuthConfig instance passed to ``from_auth_config`` Returns: Authentication strategy instance @@ -59,7 +63,7 @@ def get_strategy(self, strategy_name: str, **kwargs) -> AuthPort: ValueError: If strategy is not registered """ registration = self._get_type_registration(strategy_name) - return registration.strategy_factory(**kwargs) + return registration.strategy_factory.from_auth_config(auth_config) # type: ignore[union-attr] def list_strategies(self) -> list[str]: """ @@ -98,3 +102,8 @@ def _register_default_strategies(registry: AuthRegistry) -> None: # type: ignor from .strategy.bearer_token_strategy import BearerTokenStrategy registry.register_strategy("bearer_token", BearerTokenStrategy) + + # Register enhanced bearer token strategy + from .strategy.bearer_token_strategy_enhanced import EnhancedBearerTokenStrategy + + registry.register_strategy("bearer_token_enhanced", EnhancedBearerTokenStrategy) diff --git a/src/orb/infrastructure/auth/strategy/bearer_token_strategy.py b/src/orb/infrastructure/auth/strategy/bearer_token_strategy.py index 5d7d8745a..f2fdf00b7 100644 --- a/src/orb/infrastructure/auth/strategy/bearer_token_strategy.py +++ b/src/orb/infrastructure/auth/strategy/bearer_token_strategy.py @@ -1,9 +1,13 @@ """Bearer token authentication strategy.""" +from __future__ import annotations + import time +from typing import TYPE_CHECKING, Any import jwt +from orb.domain.base.exceptions import ConfigurationError from orb.infrastructure.adapters.ports.auth import ( AuthContext, AuthPort, @@ -12,6 +16,9 @@ ) from orb.infrastructure.logging.logger import get_logger +if TYPE_CHECKING: + pass + class BearerTokenStrategy(AuthPort): """Authentication strategy using Bearer tokens (JWT).""" @@ -177,10 +184,10 @@ async def refresh_token(self, refresh_token: str) -> AuthResult: async def revoke_token(self, token: str) -> bool: """ - Revoke token (add to blacklist). + Revoke token (add to denylist). Note: This is a simplified implementation. In production, - you would maintain a token blacklist in a database or cache. + you would maintain a token denylist in a database or cache. Args: token: Token to revoke @@ -193,6 +200,45 @@ async def revoke_token(self, token: str) -> bool: "Use EnhancedBearerTokenStrategy for revocation support." ) + @classmethod + def from_auth_config(cls, auth_config: Any) -> BearerTokenStrategy: + """ + Build strategy instance from AuthConfig. + + Extracts and validates the bearer_token sub-config, then constructs + a ``BearerTokenStrategy``. + + Args: + auth_config: AuthConfig instance with typed bearer_token sub-config + + Returns: + Configured BearerTokenStrategy + + Raises: + ConfigurationError: If required fields are missing or invalid + """ + bearer_cfg = getattr(auth_config, "bearer_token", None) + if bearer_cfg is None: + raise ConfigurationError( + "Bearer token authentication requires auth.bearer_token configuration. " + "Set auth.bearer_token.secret_key in your server config." + ) + secret_key: str = getattr(bearer_cfg, "secret_key", "") or "" + if not secret_key: + raise ConfigurationError( + "Bearer token authentication requires a secret_key in auth.bearer_token config." + ) + if len(secret_key.encode()) < 32: + raise ConfigurationError( + "Bearer token secret_key must be at least 32 bytes for security." + ) + return cls( + secret_key=secret_key, + algorithm=getattr(bearer_cfg, "algorithm", "HS256"), + token_expiry=getattr(bearer_cfg, "token_expiry", 3600), + enabled=True, + ) + def get_strategy_name(self) -> str: """ Get strategy name. diff --git a/src/orb/infrastructure/auth/strategy/bearer_token_strategy_enhanced.py b/src/orb/infrastructure/auth/strategy/bearer_token_strategy_enhanced.py index 0bb392599..361c33c75 100644 --- a/src/orb/infrastructure/auth/strategy/bearer_token_strategy_enhanced.py +++ b/src/orb/infrastructure/auth/strategy/bearer_token_strategy_enhanced.py @@ -1,21 +1,28 @@ -"""Enhanced bearer token authentication strategy with blacklist and rate limiting.""" +"""Enhanced bearer token authentication strategy with denylist and rate limiting.""" + +from __future__ import annotations import base64 import json import time from collections import defaultdict +from typing import TYPE_CHECKING, Any import jwt +from orb.domain.base.exceptions import ConfigurationError from orb.infrastructure.adapters.ports.auth import ( AuthContext, AuthPort, AuthResult, AuthStatus, ) -from orb.infrastructure.auth.token_blacklist import TokenBlacklistPort +from orb.infrastructure.auth.token_denylist import TokenDenylistPort from orb.infrastructure.logging.logger import get_logger +if TYPE_CHECKING: + pass + class RateLimiter: """Simple in-memory rate limiter for token validation. @@ -68,12 +75,12 @@ def is_rate_limited(self, identifier: str) -> bool: class EnhancedBearerTokenStrategy(AuthPort): - """Enhanced authentication strategy with JWT blacklist and rate limiting.""" + """Enhanced authentication strategy with JWT denylist and rate limiting.""" def __init__( self, secret_key: str, - blacklist: TokenBlacklistPort, + denylist: TokenDenylistPort, algorithm: str = "HS256", token_expiry: int = 3600, enabled: bool = True, @@ -86,7 +93,7 @@ def __init__( Args: secret_key: Secret key for JWT signing/verification - blacklist: Token blacklist implementation + denylist: Token denylist implementation algorithm: JWT algorithm to use token_expiry: Token expiry time in seconds enabled: Whether this strategy is enabled @@ -95,7 +102,7 @@ def __init__( rate_window: Rate limit window in seconds """ self.secret_key = secret_key - self.blacklist = blacklist + self.denylist = denylist self.algorithm = algorithm self.token_expiry = token_expiry self.enabled = enabled @@ -105,8 +112,8 @@ def __init__( # Validate secret key strength (minimum 256 bits = 32 bytes) if len(secret_key.encode()) < 32: - self.logger.warning( - "Secret key is weak (< 256 bits). Use a stronger key for production." + raise ConfigurationError( + "Bearer token secret_key must be at least 32 bytes for security." ) async def authenticate(self, context: AuthContext) -> AuthResult: @@ -157,7 +164,7 @@ async def authenticate(self, context: AuthContext) -> AuthResult: async def validate_token(self, token: str) -> AuthResult: """ - Validate JWT token with blacklist check. + Validate JWT token with denylist check. Args: token: JWT token to validate @@ -166,8 +173,8 @@ async def validate_token(self, token: str) -> AuthResult: Authentication result with user information """ try: - # Check blacklist first (fail fast) - if await self.blacklist.is_blacklisted(token): + # Check denylist first (fail fast) + if await self.denylist.is_denylisted(token): self.logger.warning("Attempted use of revoked JWT") return AuthResult( status=AuthStatus.INVALID, @@ -233,8 +240,8 @@ async def refresh_token(self, refresh_token: str) -> AuthResult: New authentication result with fresh token """ try: - # Check blacklist - if await self.blacklist.is_blacklisted(refresh_token): + # Check denylist + if await self.denylist.is_denylisted(refresh_token): return AuthResult( status=AuthStatus.INVALID, error_message="Refresh token has been revoked", @@ -281,7 +288,7 @@ async def refresh_token(self, refresh_token: str) -> AuthResult: async def revoke_token(self, token: str) -> bool: """ - Revoke token by adding to blacklist. + Revoke token by adding to denylist. Args: token: Token to revoke @@ -291,7 +298,7 @@ async def revoke_token(self, token: str) -> bool: """ try: # Extract expiration from JWT payload without verification - # (token is being revoked, we only need exp for blacklist TTL) + # (token is being revoked, we only need exp for denylist TTL) try: payload_part = token.split(".")[1] # Add padding if needed @@ -303,13 +310,13 @@ async def revoke_token(self, token: str) -> bool: except Exception: expires_at = None - # Add to blacklist - success = await self.blacklist.add_token(token, expires_at) + # Add to denylist + success = await self.denylist.add_token(token, expires_at) if success: - self.logger.info("JWT revoked and added to blacklist") + self.logger.info("JWT revoked and added to denylist") else: - self.logger.error("Failed to add JWT to blacklist") + self.logger.error("Failed to add JWT to denylist") return success @@ -317,6 +324,44 @@ async def revoke_token(self, token: str) -> bool: self.logger.error("Auth revocation error: %s", e) return False + @classmethod + def from_auth_config(cls, auth_config: Any) -> EnhancedBearerTokenStrategy: + """ + Build strategy instance from AuthConfig. + + Extracts the bearer_token sub-config and wires an InMemoryTokenDenylist. + + Args: + auth_config: AuthConfig instance with typed bearer_token sub-config + + Returns: + Configured EnhancedBearerTokenStrategy + + Raises: + ConfigurationError: If required fields are missing or invalid + """ + from orb.infrastructure.auth.token_denylist.in_memory_denylist import ( + InMemoryTokenDenylist, + ) + + bearer_cfg = getattr(auth_config, "bearer_token", None) + if bearer_cfg is None: + raise ConfigurationError( + "Enhanced bearer token authentication requires auth.bearer_token configuration." + ) + secret_key: str = getattr(bearer_cfg, "secret_key", "") or "" + if not secret_key: + raise ConfigurationError( + "Enhanced bearer token authentication requires a secret_key in auth.bearer_token config." + ) + return cls( + secret_key=secret_key, + denylist=InMemoryTokenDenylist(), + algorithm=getattr(bearer_cfg, "algorithm", "HS256"), + token_expiry=getattr(bearer_cfg, "token_expiry", 3600), + enabled=True, + ) + def get_strategy_name(self) -> str: """Get strategy name.""" return "enhanced_bearer_token" diff --git a/src/orb/infrastructure/auth/strategy/no_auth_strategy.py b/src/orb/infrastructure/auth/strategy/no_auth_strategy.py index e238802b1..4b0124b76 100644 --- a/src/orb/infrastructure/auth/strategy/no_auth_strategy.py +++ b/src/orb/infrastructure/auth/strategy/no_auth_strategy.py @@ -1,5 +1,9 @@ """No authentication strategy - allows all requests.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from orb.infrastructure.adapters.ports.auth import ( AuthContext, AuthPort, @@ -8,6 +12,9 @@ ) from orb.infrastructure.logging.logger import get_logger +if TYPE_CHECKING: + pass + class NoAuthStrategy(AuthPort): """Authentication strategy that allows all requests without authentication.""" @@ -22,6 +29,19 @@ def __init__(self, enabled: bool = False) -> None: self.enabled = enabled self.logger = get_logger(__name__) + @classmethod + def from_auth_config(cls, auth_config: Any) -> NoAuthStrategy: + """ + Build strategy instance from AuthConfig. + + Args: + auth_config: AuthConfig instance (ignored for no-auth) + + Returns: + NoAuthStrategy with enabled=False + """ + return cls(enabled=False) + async def authenticate(self, context: AuthContext) -> AuthResult: """ Allow all requests without authentication. diff --git a/src/orb/infrastructure/auth/token_blacklist/__init__.py b/src/orb/infrastructure/auth/token_blacklist/__init__.py deleted file mode 100644 index 87ad2649a..000000000 --- a/src/orb/infrastructure/auth/token_blacklist/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Token blacklist implementation for JWT revocation.""" - -from .blacklist_port import TokenBlacklistPort -from .in_memory_blacklist import InMemoryTokenBlacklist -from .redis_blacklist import RedisTokenBlacklist - -__all__ = [ - "TokenBlacklistPort", - "InMemoryTokenBlacklist", - "RedisTokenBlacklist", -] diff --git a/src/orb/infrastructure/auth/token_denylist/__init__.py b/src/orb/infrastructure/auth/token_denylist/__init__.py new file mode 100644 index 000000000..6778b8b11 --- /dev/null +++ b/src/orb/infrastructure/auth/token_denylist/__init__.py @@ -0,0 +1,11 @@ +"""Token denylist implementation for JWT revocation.""" + +from .denylist_port import TokenDenylistPort +from .in_memory_denylist import InMemoryTokenDenylist +from .redis_denylist import RedisTokenDenylist + +__all__ = [ + "TokenDenylistPort", + "InMemoryTokenDenylist", + "RedisTokenDenylist", +] diff --git a/src/orb/infrastructure/auth/token_blacklist/blacklist_port.py b/src/orb/infrastructure/auth/token_denylist/denylist_port.py similarity index 61% rename from src/orb/infrastructure/auth/token_blacklist/blacklist_port.py rename to src/orb/infrastructure/auth/token_denylist/denylist_port.py index ad3687d4f..267d57ea0 100644 --- a/src/orb/infrastructure/auth/token_blacklist/blacklist_port.py +++ b/src/orb/infrastructure/auth/token_denylist/denylist_port.py @@ -1,19 +1,19 @@ -"""Token blacklist port interface.""" +"""Token denylist port interface.""" from abc import ABC, abstractmethod from typing import Optional -class TokenBlacklistPort(ABC): - """Port interface for token blacklist implementations.""" +class TokenDenylistPort(ABC): + """Port interface for token denylist implementations.""" @abstractmethod async def add_token(self, token: str, expires_at: Optional[int] = None) -> bool: """ - Add token to blacklist. + Add token to denylist. Args: - token: Token to blacklist + token: Token to add to the denylist expires_at: Unix timestamp when token expires (for automatic cleanup) Returns: @@ -21,21 +21,21 @@ async def add_token(self, token: str, expires_at: Optional[int] = None) -> bool: """ @abstractmethod - async def is_blacklisted(self, token: str) -> bool: + async def is_denylisted(self, token: str) -> bool: """ - Check if token is blacklisted. + Check if token is on the denylist. Args: token: Token to check Returns: - True if token is blacklisted + True if token is on the denylist """ @abstractmethod async def remove_token(self, token: str) -> bool: """ - Remove token from blacklist. + Remove token from denylist. Args: token: Token to remove @@ -47,17 +47,17 @@ async def remove_token(self, token: str) -> bool: @abstractmethod async def cleanup_expired(self) -> int: """ - Remove expired tokens from blacklist. + Remove expired tokens from denylist. Returns: Number of tokens removed """ @abstractmethod - async def get_blacklist_size(self) -> int: + async def get_denylist_size(self) -> int: """ - Get number of tokens in blacklist. + Get number of tokens on the denylist. Returns: - Number of blacklisted tokens + Number of denylisted tokens """ diff --git a/src/orb/infrastructure/auth/token_blacklist/in_memory_blacklist.py b/src/orb/infrastructure/auth/token_denylist/in_memory_denylist.py similarity index 66% rename from src/orb/infrastructure/auth/token_blacklist/in_memory_blacklist.py rename to src/orb/infrastructure/auth/token_denylist/in_memory_denylist.py index 6cdc3bd8d..1764687e1 100644 --- a/src/orb/infrastructure/auth/token_blacklist/in_memory_blacklist.py +++ b/src/orb/infrastructure/auth/token_denylist/in_memory_denylist.py @@ -1,4 +1,4 @@ -"""In-memory token blacklist implementation.""" +"""In-memory token denylist implementation.""" import asyncio import time @@ -6,78 +6,78 @@ from orb.infrastructure.logging.logger import get_logger -from .blacklist_port import TokenBlacklistPort +from .denylist_port import TokenDenylistPort -class InMemoryTokenBlacklist(TokenBlacklistPort): - """In-memory token blacklist with automatic cleanup.""" +class InMemoryTokenDenylist(TokenDenylistPort): + """In-memory token denylist with automatic cleanup.""" def __init__(self, cleanup_interval: int = 3600) -> None: """ - Initialize in-memory blacklist. + Initialize in-memory denylist. Args: cleanup_interval: Interval in seconds for automatic cleanup """ - self._blacklist: Dict[str, Optional[int]] = {} + self._denylist: Dict[str, Optional[int]] = {} self._cleanup_interval = cleanup_interval self._cleanup_task: Optional[asyncio.Task] = None self._logger = get_logger(__name__) self._lock = asyncio.Lock() async def add_token(self, token: str, expires_at: Optional[int] = None) -> bool: - """Add token to blacklist.""" + """Add token to denylist.""" async with self._lock: - self._blacklist[token] = expires_at - self._logger.info("Token added to blacklist (expires_at=%s)", expires_at) + self._denylist[token] = expires_at + self._logger.info("Token added to denylist (expires_at=%s)", expires_at) return True - async def is_blacklisted(self, token: str) -> bool: - """Check if token is blacklisted.""" + async def is_denylisted(self, token: str) -> bool: + """Check if token is on the denylist.""" async with self._lock: - if token not in self._blacklist: + if token not in self._denylist: return False # Check if token has expired - expires_at = self._blacklist[token] + expires_at = self._denylist[token] if expires_at and time.time() > expires_at: - # Token expired, remove from blacklist - del self._blacklist[token] + # Token expired, remove from denylist + del self._denylist[token] return False return True async def remove_token(self, token: str) -> bool: - """Remove token from blacklist.""" + """Remove token from denylist.""" async with self._lock: - if token in self._blacklist: - del self._blacklist[token] - self._logger.info("Token removed from blacklist") + if token in self._denylist: + del self._denylist[token] + self._logger.info("Token removed from denylist") return True return False async def cleanup_expired(self) -> int: - """Remove expired tokens from blacklist.""" + """Remove expired tokens from denylist.""" async with self._lock: current_time = time.time() expired_tokens = [ token - for token, expires_at in self._blacklist.items() + for token, expires_at in self._denylist.items() if expires_at and current_time > expires_at ] for token in expired_tokens: - del self._blacklist[token] + del self._denylist[token] if expired_tokens: self._logger.info("Cleaned up %d expired tokens", len(expired_tokens)) return len(expired_tokens) - async def get_blacklist_size(self) -> int: - """Get number of tokens in blacklist.""" + async def get_denylist_size(self) -> int: + """Get number of tokens on the denylist.""" async with self._lock: - return len(self._blacklist) + return len(self._denylist) async def start_cleanup_task(self) -> None: """Start automatic cleanup task.""" diff --git a/src/orb/infrastructure/auth/token_blacklist/redis_blacklist.py b/src/orb/infrastructure/auth/token_denylist/redis_denylist.py similarity index 65% rename from src/orb/infrastructure/auth/token_blacklist/redis_blacklist.py rename to src/orb/infrastructure/auth/token_denylist/redis_denylist.py index 78d5b59b2..b7d1e579b 100644 --- a/src/orb/infrastructure/auth/token_blacklist/redis_blacklist.py +++ b/src/orb/infrastructure/auth/token_denylist/redis_denylist.py @@ -1,19 +1,19 @@ -"""Redis-based token blacklist implementation.""" +"""Redis-based token denylist implementation.""" import time from typing import Optional from orb.infrastructure.logging.logger import get_logger -from .blacklist_port import TokenBlacklistPort +from .denylist_port import TokenDenylistPort -class RedisTokenBlacklist(TokenBlacklistPort): - """Redis-based token blacklist with automatic expiration.""" +class RedisTokenDenylist(TokenDenylistPort): + """Redis-based token denylist with automatic expiration.""" - def __init__(self, redis_client=None, key_prefix: str = "token_blacklist:") -> None: + def __init__(self, redis_client=None, key_prefix: str = "token_denylist:") -> None: """ - Initialize Redis blacklist. + Initialize Redis denylist. Args: redis_client: Redis client instance (optional, will use in-memory if None) @@ -26,12 +26,12 @@ def __init__(self, redis_client=None, key_prefix: str = "token_blacklist:") -> N if self._redis is None: self._logger.warning("Redis client not provided, using in-memory fallback") - from .in_memory_blacklist import InMemoryTokenBlacklist + from .in_memory_denylist import InMemoryTokenDenylist - self._fallback = InMemoryTokenBlacklist() + self._fallback = InMemoryTokenDenylist() async def add_token(self, token: str, expires_at: Optional[int] = None) -> bool: - """Add token to blacklist.""" + """Add token to denylist.""" if self._fallback: return await self._fallback.add_token(token, expires_at) @@ -46,17 +46,17 @@ async def add_token(self, token: str, expires_at: Optional[int] = None) -> bool: # No expiration, set indefinitely await self._redis.set(key, "1") # type: ignore[union-attr] - self._logger.info("Token added to Redis blacklist (expires_at=%s)", expires_at) + self._logger.info("Token added to Redis denylist (expires_at=%s)", expires_at) return True except Exception as e: - self._logger.error("Failed to add token to Redis blacklist: %s", e) + self._logger.error("Failed to add token to Redis denylist: %s", e) return False - async def is_blacklisted(self, token: str) -> bool: - """Check if token is blacklisted.""" + async def is_denylisted(self, token: str) -> bool: + """Check if token is on the denylist.""" if self._fallback: - return await self._fallback.is_blacklisted(token) + return await self._fallback.is_denylisted(token) try: key = f"{self._key_prefix}{token}" @@ -64,28 +64,28 @@ async def is_blacklisted(self, token: str) -> bool: return bool(result) except Exception as e: - self._logger.error("Failed to check token in Redis blacklist: %s", e) - # Fail secure: assume token is blacklisted on error + self._logger.error("Failed to check token in Redis denylist: %s", e) + # Fail secure: assume token is denylisted on error return True async def remove_token(self, token: str) -> bool: - """Remove token from blacklist.""" + """Remove token from denylist.""" if self._fallback: return await self._fallback.remove_token(token) try: key = f"{self._key_prefix}{token}" result = await self._redis.delete(key) # type: ignore[union-attr] - self._logger.info("Token removed from Redis blacklist") + self._logger.info("Token removed from Redis denylist") return bool(result) except Exception as e: - self._logger.error("Failed to remove token from Redis blacklist: %s", e) + self._logger.error("Failed to remove token from Redis denylist: %s", e) return False async def cleanup_expired(self) -> int: """ - Remove expired tokens from blacklist. + Remove expired tokens from denylist. Note: Redis automatically removes expired keys, so this is a no-op. """ @@ -95,10 +95,10 @@ async def cleanup_expired(self) -> int: # Redis handles expiration automatically return 0 - async def get_blacklist_size(self) -> int: - """Get number of tokens in blacklist.""" + async def get_denylist_size(self) -> int: + """Get number of tokens on the denylist.""" if self._fallback: - return await self._fallback.get_blacklist_size() + return await self._fallback.get_denylist_size() try: # Count keys matching our prefix @@ -107,5 +107,5 @@ async def get_blacklist_size(self) -> int: return len(keys) except Exception as e: - self._logger.error("Failed to get blacklist size from Redis: %s", e) + self._logger.error("Failed to get denylist size from Redis: %s", e) return 0 diff --git a/src/orb/infrastructure/registry/cli_spec_registry.py b/src/orb/infrastructure/registry/cli_spec_registry.py new file mode 100644 index 000000000..209fb500b --- /dev/null +++ b/src/orb/infrastructure/registry/cli_spec_registry.py @@ -0,0 +1,24 @@ +"""CLI spec registry — maps provider type strings to ProviderCLISpecPort instances.""" + +from orb.domain.base.ports.provider_cli_spec_port import ProviderCLISpecPort + + +class CLISpecRegistry: + """Simple registry mapping provider type strings to ProviderCLISpecPort instances.""" + + _specs: dict[str, ProviderCLISpecPort] = {} + + @classmethod + def register(cls, provider_type: str, spec: ProviderCLISpecPort) -> None: + """Register a CLI spec for a provider type.""" + cls._specs[provider_type] = spec + + @classmethod + def get(cls, provider_type: str) -> ProviderCLISpecPort | None: + """Return the CLI spec for a provider type, or None if not registered.""" + return cls._specs.get(provider_type) + + @classmethod + def all(cls) -> dict[str, ProviderCLISpecPort]: + """Return all registered CLI specs.""" + return dict(cls._specs) diff --git a/src/orb/infrastructure/registry/template_extension_registry.py b/src/orb/infrastructure/registry/template_extension_registry.py new file mode 100644 index 000000000..b29cf891d --- /dev/null +++ b/src/orb/infrastructure/registry/template_extension_registry.py @@ -0,0 +1,178 @@ +"""Template extension registry for provider-specific extensions.""" + +from typing import Any, Optional + +from pydantic import BaseModel + +from orb.domain.template.extensions import TemplateExtension + + +class TemplateExtensionRegistry: + """Registry for provider-specific template extensions. + + This registry follows the Open/Closed Principle - it's open for extension + (new providers can be added) but closed for modification (existing code + doesn't need to change when new providers are added). + """ + + _extensions: dict[str, type[BaseModel]] = {} + _extension_instances: dict[str, TemplateExtension] = {} + + @classmethod + def register_extension(cls, provider_type: str, extension_class: type[BaseModel]) -> None: + """Register a provider-specific extension configuration class. + + Args: + provider_type: The provider type (e.g., 'aws', 'provider1', 'provider2') + extension_class: The Pydantic model class for the extension configuration + """ + if not issubclass(extension_class, BaseModel): + raise ValueError(f"Extension class must be a Pydantic BaseModel, got {extension_class}") + + cls._extensions[provider_type] = extension_class + + @classmethod + def register_extension_instance( + cls, provider_type: str, extension_instance: TemplateExtension + ) -> None: + """Register a provider-specific extension instance. + + Args: + provider_type: The provider type (e.g., 'aws', 'provider1', 'provider2') + extension_instance: The extension instance implementing TemplateExtension + """ + if not isinstance(extension_instance, TemplateExtension): + raise ValueError( + f"Extension instance must implement TemplateExtension, got {type(extension_instance)}" + ) + + cls._extension_instances[provider_type] = extension_instance + + @classmethod + def get_extension_class(cls, provider_type: str) -> Optional[type[BaseModel]]: + """Get extension configuration class for a provider. + + Args: + provider_type: The provider type to get extension class for + + Returns: + The extension configuration class or None if not registered + """ + return cls._extensions.get(provider_type) + + @classmethod + def get_extension_instance(cls, provider_type: str) -> Optional[TemplateExtension]: + """Get extension instance for a provider. + + Args: + provider_type: The provider type to get extension instance for + + Returns: + The extension instance or None if not registered + """ + return cls._extension_instances.get(provider_type) + + @classmethod + def has_extension(cls, provider_type: str) -> bool: + """Check if a provider has registered extensions. + + Args: + provider_type: The provider type to check + + Returns: + True if the provider has registered extensions + """ + return provider_type in cls._extensions or provider_type in cls._extension_instances + + @classmethod + def get_supported_providers(cls) -> list[str]: + """Get list of providers with registered extensions. + + Returns: + List of provider types that have registered extensions + """ + all_providers = set(cls._extensions.keys()) | set(cls._extension_instances.keys()) + return list(all_providers) + + @classmethod + def create_extension_config( + cls, provider_type: str, config_data: dict[str, Any] + ) -> Optional[BaseModel]: + """Create an extension configuration instance for a provider. + + Args: + provider_type: The provider type + config_data: The configuration data to create the extension with + + Returns: + The created extension configuration instance or None if provider not registered + """ + extension_class = cls.get_extension_class(provider_type) + if extension_class: + return extension_class(**config_data) + return None + + @classmethod + def get_extension_defaults( + cls, provider_type: str, config_data: Optional[dict[str, Any]] = None + ) -> dict[str, Any]: + """Get template defaults from provider extension. + + Args: + provider_type: The provider type + config_data: Optional configuration data to create extension with + + Returns: + Dictionary of template defaults from the extension + """ + # Try extension instance first + extension_instance = cls.get_extension_instance(provider_type) + if extension_instance: + return extension_instance.to_template_defaults() # type: ignore[attr-defined] + + # Try creating from extension class + if config_data: + extension_config = cls.create_extension_config(provider_type, config_data) + if extension_config and hasattr(extension_config, "to_template_defaults"): + return extension_config.to_template_defaults() # type: ignore[attr-defined] + + return {} + + @classmethod + def clear_registry(cls) -> None: + """Clear all registered extensions (mainly for testing).""" + cls._extensions.clear() + cls._extension_instances.clear() + + +class TemplateExtensionRegistryAdapter: + """Instance adapter that satisfies ``TemplateExtensionRegistryPort``. + + Delegates every call to the ``TemplateExtensionRegistry`` classmethods so + that callers in the application layer receive an ordinary injected object + rather than reaching for the global registry directly. + """ + + def get_extension_defaults( + self, provider_type: str, config_data: Optional[dict[str, Any]] = None + ) -> dict[str, Any]: + """Delegate to ``TemplateExtensionRegistry.get_extension_defaults``.""" + return TemplateExtensionRegistry.get_extension_defaults(provider_type, config_data) + + +# Convenience functions for common operations +def register_provider_extension(provider_type: str, extension_class: type[BaseModel]) -> None: + """Register a provider extension.""" + TemplateExtensionRegistry.register_extension(provider_type, extension_class) + + +def get_provider_extension_defaults( + provider_type: str, config_data: Optional[dict[str, Any]] = None +) -> dict[str, Any]: + """Get provider extension defaults.""" + return TemplateExtensionRegistry.get_extension_defaults(provider_type, config_data) + + +def has_provider_extension(provider_type: str) -> bool: + """Check if provider has extensions.""" + return TemplateExtensionRegistry.has_extension(provider_type) diff --git a/src/orb/infrastructure/scheduler/hostfactory/field_mapper.py b/src/orb/infrastructure/scheduler/hostfactory/field_mapper.py index 21d05cc3c..1417ffc1b 100644 --- a/src/orb/infrastructure/scheduler/hostfactory/field_mapper.py +++ b/src/orb/infrastructure/scheduler/hostfactory/field_mapper.py @@ -9,13 +9,13 @@ class HostFactoryFieldMapper(SchedulerFieldMapper): """HostFactory-specific field mapping and transformations.""" - def __init__(self, provider_type: str = "aws"): + def __init__(self, provider_type: str | None = None): self.provider_type = provider_type @property def field_mappings(self) -> Dict[str, str]: """Get HostFactory field mappings for the provider.""" - return HostFactoryFieldMappings.get_mappings(self.provider_type) + return HostFactoryFieldMappings.get_mappings(self.provider_type or "") def map_input_fields(self, external_template: Dict[str, Any]) -> Dict[str, Any]: """Map HostFactory format → internal format with transformations.""" @@ -141,13 +141,26 @@ def _apply_output_transformations(self, mapped: Dict[str, Any]) -> Dict[str, Any return {k: v for k, v in mapped.items() if v is not None and v != {} and v != []} def _create_hf_attributes(self, instance_type: str) -> Dict[str, List[str]]: - """Create HostFactory attributes from instance type.""" - from orb.providers.aws.utilities.ec2.instances import derive_cpu_ram_from_instance_type + """Create HostFactory attributes from instance type via the per-provider registry. + + Delegates to the registered ``FieldMappingPort.derive_attributes`` for + the active provider type. Falls back to a minimal placeholder when no + adapter is registered (non-AWS providers that don't supply cpu/ram + lookup yet). + """ + from orb.infrastructure.scheduler.hostfactory.field_mapping_registry import ( + FieldMappingRegistry, + ) - ncpus, nram = derive_cpu_ram_from_instance_type(instance_type) + adapter = FieldMappingRegistry.get(self.provider_type or "") + if adapter is not None: + result = adapter.derive_attributes(instance_type) + if result is not None: + return result # type: ignore[return-value] + # Fallback: return a minimal attributes object so HF spec is still satisfied. return { "type": ["String", "X86_64"], - "ncpus": ["Numeric", str(ncpus)], - "nram": ["Numeric", str(nram)], + "ncpus": ["Numeric", "1"], + "nram": ["Numeric", "1024"], } diff --git a/src/orb/infrastructure/scheduler/hostfactory/field_mapping_port.py b/src/orb/infrastructure/scheduler/hostfactory/field_mapping_port.py new file mode 100644 index 000000000..f8ffc32fd --- /dev/null +++ b/src/orb/infrastructure/scheduler/hostfactory/field_mapping_port.py @@ -0,0 +1,46 @@ +"""Protocol defining per-provider field-mapping behaviour for the HostFactory scheduler.""" + +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class FieldMappingPort(Protocol): + """Per-provider HostFactory field-mapping extension point. + + Implementations live under ``providers//scheduler/`` and are + registered via ``FieldMappingRegistry`` during provider bootstrap. + """ + + def get_mappings(self) -> dict[str, str]: + """Return the provider-specific HF-field → internal-field name dict. + + The shared ``HostFactoryFieldMappings.MAPPINGS["generic"]`` table is + always applied first; this method returns only the *additional* + provider-specific entries that should be merged on top. + """ + ... # type: ignore[return] + + def apply_defaults(self, mapped: dict) -> dict: + """Apply provider-specific ``setdefault`` logic after field mapping. + + Args: + mapped: The partially-mapped template dict (mutated in-place and + returned for convenience). + + Returns: + The same dict with provider defaults applied. + """ + ... # type: ignore[return] + + def derive_attributes(self, machine_type: str | None) -> dict[str, list[str]] | None: + """Build the HF ``attributes`` object for a given machine / instance type. + + Args: + machine_type: Provider-specific instance/machine type string + (e.g. ``"t3.medium"`` for AWS EC2). + + Returns: + A dict suitable for the HF ``attributes`` field, or ``None`` when + the provider does not support cpu/ram attribute derivation. + """ + ... # type: ignore[return] diff --git a/src/orb/infrastructure/scheduler/hostfactory/field_mapping_registry.py b/src/orb/infrastructure/scheduler/hostfactory/field_mapping_registry.py new file mode 100644 index 000000000..419c0230d --- /dev/null +++ b/src/orb/infrastructure/scheduler/hostfactory/field_mapping_registry.py @@ -0,0 +1,47 @@ +"""Registry mapping provider types to FieldMappingPort instances.""" + +from orb.infrastructure.scheduler.hostfactory.field_mapping_port import FieldMappingPort + + +class FieldMappingRegistry: + """Simple class-variable registry mapping provider type strings to + ``FieldMappingPort`` implementations. + + Follows the same lightweight pattern as ``CLISpecRegistry``. + + Usage:: + + # During provider bootstrap: + FieldMappingRegistry.register("aws", AWSFieldMapping()) + + # At call site: + adapter = FieldMappingRegistry.get("aws") + if adapter is not None: + mapped = adapter.apply_defaults(mapped) + """ + + _adapters: dict[str, FieldMappingPort] = {} + + @classmethod + def register(cls, provider_type: str, adapter: FieldMappingPort) -> None: + """Register a field-mapping adapter for *provider_type*. + + Registration is idempotent — re-registering the same provider type + silently overwrites the previous entry. + """ + cls._adapters[provider_type] = adapter + + @classmethod + def get(cls, provider_type: str) -> FieldMappingPort | None: + """Return the adapter for *provider_type*, or ``None`` if not registered.""" + return cls._adapters.get(provider_type) + + @classmethod + def registered_providers(cls) -> list[str]: + """Return all registered provider type strings.""" + return list(cls._adapters.keys()) + + @classmethod + def clear(cls) -> None: + """Remove all registrations (primarily for use in tests).""" + cls._adapters.clear() diff --git a/src/orb/infrastructure/scheduler/hostfactory/field_mappings.py b/src/orb/infrastructure/scheduler/hostfactory/field_mappings.py index ea8135ed3..4e82232ac 100644 --- a/src/orb/infrastructure/scheduler/hostfactory/field_mappings.py +++ b/src/orb/infrastructure/scheduler/hostfactory/field_mappings.py @@ -6,7 +6,15 @@ class HostFactoryFieldMappings: """Registry of HostFactory-specific field mappings per provider.""" - # Field mappings organized by provider + # Field mappings organized by provider. + # + # NOTE: AWS entries here are kept as a no-bootstrap fallback so static + # consumers (e.g. unit tests that exercise the mapper without booting the + # AWS provider) keep working. The canonical AWS mapping lives in + # ``providers/aws/scheduler/hostfactory_field_mapping.py:AWSFieldMapping`` + # registered into ``FieldMappingRegistry`` during provider bootstrap. + # Adding a new provider should NOT add entries here — register a + # ``FieldMappingPort`` adapter instead. MAPPINGS = { # Generic fields (work with any provider) "generic": { @@ -95,23 +103,6 @@ def get_supported_providers(cls) -> list[str]: """Get list of supported providers for HostFactory.""" return [key for key in cls.MAPPINGS.keys() if key != "generic"] - @classmethod - def apply_aws_defaults(cls, mapped: dict) -> dict: - """ - Apply AWS-specific setdefault logic that depends on launch_template_id. - - When launch_template_id is set the LT already encodes network config, - so subnet_ids and security_group_ids should not be defaulted to empty lists. - """ - mapped.setdefault("max_instances", 1) - mapped.setdefault("price_type", "ondemand") - mapped.setdefault("allocation_strategy", "lowestPrice") - if not mapped.get("launch_template_id"): - mapped.setdefault("subnet_ids", []) - mapped.setdefault("security_group_ids", []) - mapped.setdefault("tags", {}) - return mapped - @classmethod def is_provider_specific_field(cls, provider_type: str, field_name: str) -> bool: """ diff --git a/src/orb/infrastructure/scheduler/hostfactory/hostfactory_strategy.py b/src/orb/infrastructure/scheduler/hostfactory/hostfactory_strategy.py index 6db3f6d92..257d5b52d 100644 --- a/src/orb/infrastructure/scheduler/hostfactory/hostfactory_strategy.py +++ b/src/orb/infrastructure/scheduler/hostfactory/hostfactory_strategy.py @@ -12,7 +12,6 @@ from orb.application.request.dto import RequestDTO from orb.infrastructure.scheduler.base.strategy import BaseSchedulerStrategy from orb.infrastructure.scheduler.hostfactory.field_mapper import HostFactoryFieldMapper -from orb.infrastructure.scheduler.hostfactory.field_mappings import HostFactoryFieldMappings from orb.infrastructure.scheduler.hostfactory.transformations import HostFactoryTransformations from orb.infrastructure.template.dtos import TemplateDTO from orb.infrastructure.utilities.common.string_utils import extract_provider_type @@ -140,7 +139,15 @@ def _map_template_fields( if "template_id" in mapped: mapped["name"] = template.get("name", mapped["template_id"]) - HostFactoryFieldMappings.apply_aws_defaults(mapped) + # Apply per-provider field defaults via the registry. + provider_type = self._get_active_provider_type() + from orb.infrastructure.scheduler.hostfactory.field_mapping_registry import ( + FieldMappingRegistry, + ) + + adapter = FieldMappingRegistry.get(provider_type) + if adapter is not None: + adapter.apply_defaults(mapped) mapped["created_at"] = template.get("created_at") mapped["updated_at"] = template.get("updated_at") @@ -592,14 +599,28 @@ def format_return_requests_response(self, requests: list[Any]) -> dict[str, Any] } def _build_hf_attributes(self, instance_type: str) -> dict[str, list[str]]: - """Build IBM HF attributes dict from an instance type string.""" - from orb.providers.aws.utilities.ec2.instances import derive_cpu_ram_from_instance_type + """Build IBM HF attributes dict from an instance type string. - ncpus, nram = derive_cpu_ram_from_instance_type(instance_type) + Delegates to the per-provider ``FieldMappingPort.derive_attributes`` + via the registry. Falls back to a minimal placeholder so callers + always receive a valid attributes object. + """ + from orb.infrastructure.scheduler.hostfactory.field_mapping_registry import ( + FieldMappingRegistry, + ) + + provider_type = self._get_active_provider_type() + adapter = FieldMappingRegistry.get(provider_type) + if adapter is not None: + result = adapter.derive_attributes(instance_type) + if result is not None: + return result + + # Fallback: return a minimal attributes object so HF spec is still satisfied. return { "type": ["String", "X86_64"], - "ncpus": ["Numeric", str(ncpus)], - "nram": ["Numeric", str(nram)], + "ncpus": ["Numeric", "1"], + "nram": ["Numeric", "1024"], } def format_templates_for_dispatch(self, templates: list[dict]) -> list[dict]: @@ -654,6 +675,9 @@ def format_request_status_response(self, requests: list[RequestDTO]) -> dict[str ), "message": req_dict.get("message", ""), "machines": machines, + # ORB extension: expose resource_ids so callers can discover the backing + # fleet / ASG identifier without a separate EC2 tag lookup. + "resource_ids": req_dict.get("resource_ids") or [], } # Add provider information if present @@ -664,6 +688,20 @@ def format_request_status_response(self, requests: list[RequestDTO]) -> dict[str if req_dict.get("provider_api"): hf_request["providerApi"] = req_dict["provider_api"] + # Surface ProviderFulfilment capacity fields so callers can verify + # weighted-fleet fulfilment (target capacity units vs delivered). + # IBM Symphony spec is silent on these fields; expose as ORB + # extension keys when the provider has populated them. + for cap_field in ( + "target_units", + "fulfilled_units", + "running_count", + "pending_count", + ): + value = req_dict.get(cap_field) + if value is not None: + hf_request[cap_field] = value + formatted_requests.append(hf_request) return {"requests": formatted_requests} diff --git a/src/orb/infrastructure/storage/registration.py b/src/orb/infrastructure/storage/registration.py index 63ca3bfc4..e91f7f83b 100644 --- a/src/orb/infrastructure/storage/registration.py +++ b/src/orb/infrastructure/storage/registration.py @@ -22,13 +22,16 @@ def register_all_storage_types() -> None: register_sql_storage() - from orb.providers.aws.storage.registration import ( - register_aurora_storage, - register_dynamodb_storage, - ) - - register_dynamodb_storage() - register_aurora_storage() + try: + from orb.providers.aws.storage.registration import ( + register_aurora_storage, + register_dynamodb_storage, + ) + + register_dynamodb_storage() + register_aurora_storage() + except ImportError: + pass # [aws] extra not installed; DynamoDB and Aurora storage unavailable def get_available_storage_types() -> list: diff --git a/src/orb/infrastructure/template/dtos.py b/src/orb/infrastructure/template/dtos.py index 2fd2ca335..52bf122f8 100644 --- a/src/orb/infrastructure/template/dtos.py +++ b/src/orb/infrastructure/template/dtos.py @@ -4,9 +4,10 @@ from datetime import datetime from typing import Any, Optional -from pydantic import Field, model_validator +from pydantic import BaseModel, Field, field_serializer, model_validator from orb.application.dto.base import BaseDTO +from orb.infrastructure.registry.template_extension_registry import TemplateExtensionRegistry class TemplateDTO(BaseDTO): @@ -56,7 +57,6 @@ class TemplateDTO(BaseDTO): key_name: Optional[str] = None user_data: Optional[str] = None instance_profile: Optional[str] = None - launch_template_id: Optional[str] = None # Advanced configuration monitoring_enabled: Optional[bool] = None @@ -65,6 +65,9 @@ class TemplateDTO(BaseDTO): tags: dict[str, Any] = Field(default_factory=dict) metadata: dict[str, Any] = Field(default_factory=dict) + # Typed provider-specific configuration, populated via TemplateExtensionRegistry + provider_config: Optional[BaseModel] = None + # Provider-specific data (keyed by provider name, e.g. {"aws": {...}}) provider_data: dict[str, Any] = Field(default_factory=dict) @@ -83,6 +86,13 @@ class TemplateDTO(BaseDTO): # Legacy fields version: Optional[str] = None + @field_serializer("provider_config") + def _serialize_provider_config(self, value: Optional[BaseModel]) -> Optional[dict[str, Any]]: + """Serialise the typed provider_config to a plain dict for model_dump() consumers.""" + if value is None: + return None + return value.model_dump() + @model_validator(mode="before") @classmethod def _set_defaults(cls, data: Any) -> Any: @@ -95,28 +105,21 @@ def _set_defaults(cls, data: Any) -> Any: @classmethod def from_domain(cls, template) -> "TemplateDTO": """Convert domain template to DTO.""" - # Pack AWS-specific fields into metadata. - _fleet_type = getattr(template, "fleet_type", None) - _fleet_type_str: Optional[str] = ( - str(_fleet_type.value) - if _fleet_type is not None and hasattr(_fleet_type, "value") - else (_fleet_type if _fleet_type is None else str(_fleet_type)) - ) - _abis = getattr(template, "abis_instance_requirements", None) - _fleet_role = getattr(template, "fleet_role", None) - _percent_on_demand = getattr(template, "percent_on_demand", None) - - aws_extras: dict[str, Any] = {} - if _fleet_type_str is not None: - aws_extras["fleet_type"] = _fleet_type_str - if _fleet_role is not None: - aws_extras["fleet_role"] = _fleet_role - if _percent_on_demand is not None: - aws_extras["percent_on_demand"] = _percent_on_demand - if _abis is not None: - aws_extras["abis_instance_requirements"] = _abis.to_aws_dict() - - metadata = {**getattr(template, "metadata", {}), **aws_extras} + # Delegate provider-specific field extraction to the extension registry. + # Each provider registers an extension class that knows which fields to + # capture. Infrastructure layer stays provider-agnostic. + _provider_type = getattr(template, "provider_type", None) + _provider_config: Optional[BaseModel] = None + if _provider_type: + # Serialise the domain object to a plain dict so the extension class + # can pick up its own fields (with extra="ignore"). + if hasattr(template, "model_dump"): + _template_data = template.model_dump() + else: + _template_data = vars(template) + _provider_config = TemplateExtensionRegistry.create_extension_config( + _provider_type, _template_data + ) return cls( # Core fields @@ -151,15 +154,16 @@ def from_domain(cls, template) -> "TemplateDTO": key_name=getattr(template, "key_name", None), user_data=getattr(template, "user_data", None), instance_profile=getattr(template, "instance_profile", None), - launch_template_id=getattr(template, "launch_template_id", None), # Advanced configuration monitoring_enabled=getattr(template, "monitoring_enabled", None), - # Tags and metadata + # Tags and metadata (cross-provider opaque data only) tags=getattr(template, "tags", {}), - metadata=metadata, + metadata=getattr(template, "metadata", {}), + # Typed provider-specific configuration (populated via registry) + provider_config=_provider_config, provider_data=getattr(template, "provider_data", {}), - # Provider configuration - provider_type=getattr(template, "provider_type", None), + # Provider identification + provider_type=_provider_type, provider_name=getattr(template, "provider_name", None), provider_api=getattr(template, "provider_api", None), # Timestamps diff --git a/src/orb/interface/infrastructure_command_handler.py b/src/orb/interface/infrastructure_command_handler.py index 60ff7bbba..46f460771 100644 --- a/src/orb/interface/infrastructure_command_handler.py +++ b/src/orb/interface/infrastructure_command_handler.py @@ -7,6 +7,7 @@ from orb.domain.base.ports.console_port import ConsolePort from orb.infrastructure.di.container import get_container from orb.infrastructure.error.decorators import handle_interface_exceptions +from orb.infrastructure.registry.cli_spec_registry import CLISpecRegistry @handle_interface_exceptions(context="infrastructure_discover", interface_type="cli") @@ -113,8 +114,15 @@ def _show_provider_infrastructure(provider: Dict[str, Any]) -> None: config = provider.get("config", {}) if config: - console.info(f"Region: {config.get('region', 'N/A')}") - console.info(f"Profile: {config.get('profile', 'N/A')}") + provider_type = provider.get("type", "") + spec = CLISpecRegistry.get(provider_type) + if spec is not None: + for label, value in spec.format_display(config): + console.info(f"{label}: {value}") + else: + for key, value in config.items(): + label = key.replace("_", " ").title() + console.info(f"{label}: {value}") template_defaults = provider.get("template_defaults", {}) if template_defaults: @@ -174,7 +182,11 @@ def _get_active_providers() -> List[Dict[str, Any]]: registry_service = get_container().get(ProviderRegistryService) registered_types = registry_service.get_registered_provider_types() - default_type = registered_types[0] if registered_types else "aws" + if not registered_types: + raise RuntimeError( + "No providers are registered. Run 'orb init' to configure a provider." + ) + default_type = registered_types[0] return [ { "name": "default", @@ -199,7 +211,11 @@ def _get_active_providers() -> List[Dict[str, Any]]: registry_service = get_container().get(ProviderRegistryService) registered_types = registry_service.get_registered_provider_types() - default_type = registered_types[0] if registered_types else "aws" + if not registered_types: + raise RuntimeError( + "No providers are registered. Run 'orb init' to configure a provider." + ) + default_type = registered_types[0] active_providers = [ { "name": "default", diff --git a/src/orb/interface/init_command_handler.py b/src/orb/interface/init_command_handler.py index 632aaf428..4bbfe2f68 100644 --- a/src/orb/interface/init_command_handler.py +++ b/src/orb/interface/init_command_handler.py @@ -16,6 +16,7 @@ from orb.domain.base.ports.provider_registry_port import ProviderRegistryPort from orb.infrastructure.di.container import get_container from orb.infrastructure.logging.logger import get_logger +from orb.infrastructure.registry.cli_spec_registry import CLISpecRegistry logger = get_logger(__name__) @@ -602,10 +603,22 @@ def _get_default_config(args) -> Dict[str, Any]: strategy.get_cli_infrastructure_defaults(args) if strategy is not None else {} ) + # Use the registered CLI spec to extract provider config when available. + # Fall back to the init-level --profile / --region flags for backward + # compatibility with callers that have not yet adopted provider-specific flags. + spec = CLISpecRegistry.get(provider_type) + if spec is not None: + spec_config = spec.extract_config(args) + profile = spec_config.get("profile") or getattr(args, "profile", None) or None + region = spec_config.get("region") or getattr(args, "region", None) or default_region + else: + profile = getattr(args, "profile", None) or None + region = getattr(args, "region", None) or default_region + first_provider = { "type": provider_type, - "profile": args.profile or None, - "region": args.region or default_region, + "profile": profile, + "region": region, "infrastructure_defaults": infrastructure_defaults, } diff --git a/src/orb/interface/mcp/server/handler.py b/src/orb/interface/mcp/server/handler.py index b0e5c06d4..8630e1f7e 100644 --- a/src/orb/interface/mcp/server/handler.py +++ b/src/orb/interface/mcp/server/handler.py @@ -4,6 +4,7 @@ import sys from typing import Any +from orb.bootstrap import Application from orb.infrastructure.di.container import get_container from orb.infrastructure.error.decorators import handle_interface_exceptions from orb.infrastructure.logging.logger import get_logger @@ -29,11 +30,16 @@ async def handle_mcp_serve(args) -> dict[str, Any]: host = getattr(args, "host", "localhost") stdio_mode = getattr(args, "stdio", False) - # Get application instance from DI container - container = get_container() + # Bootstrap the application so that the DI container has all providers, + # CQRS handlers, and configuration registered. Without this the MCP tool + # handlers operate against a bare container and every request fails with + # "No strategy found for provider". + app = Application() + if not await app.initialize(): + raise RuntimeError("Failed to initialize ORB application for MCP server") - # Create MCP server instance - mcp_server = OpenResourceBrokerMCPServer(app=container) + # Create MCP server instance with the initialized DI container + mcp_server = OpenResourceBrokerMCPServer(app=get_container()) if stdio_mode: # Run in stdio mode for direct MCP client communication diff --git a/src/orb/interface/provider_config_handler.py b/src/orb/interface/provider_config_handler.py index 25b6e3206..09f0e8c96 100644 --- a/src/orb/interface/provider_config_handler.py +++ b/src/orb/interface/provider_config_handler.py @@ -5,10 +5,10 @@ from orb.application.dto.interface_response import InterfaceResponse from orb.config.platform_dirs import get_config_location -from orb.domain.base.ports.provider_cli_spec_port import CLISpecRegistry from orb.infrastructure.di.container import get_container from orb.infrastructure.error.decorators import handle_interface_exceptions from orb.infrastructure.logging.logger import get_logger +from orb.infrastructure.registry.cli_spec_registry import CLISpecRegistry from orb.interface.response_formatting_service import ResponseFormattingService logger = get_logger(__name__) @@ -29,7 +29,13 @@ async def handle_provider_add(args) -> dict[str, Any]: with open(config_file) as f: config = json.load(f) - provider_type = getattr(args, "provider_type", "aws") + provider_type = getattr(args, "provider_type", None) + if not provider_type: + return { + "error": True, + "message": "Provider type is required. Specify --provider-type.", + "exit_code": 1, + } spec = CLISpecRegistry.get(provider_type) if spec is None: @@ -152,27 +158,21 @@ async def handle_provider_update(args) -> dict[str, Any]: } # Infer provider type from stored record - provider_type = provider.get("type", "aws") + provider_type = provider.get("type") or "" spec = CLISpecRegistry.get(provider_type) provider_config = provider.get("config", {}) - if spec is not None: - partial = spec.extract_partial_config(args) - if not partial: - return {"error": True, "message": "No updates specified.", "exit_code": 1} - provider_config.update(partial) - else: - # Fallback: apply any non-None aws_* attrs directly - updated = False - if getattr(args, "aws_region", None): - provider_config["region"] = args.aws_region - updated = True - if getattr(args, "aws_profile", None): - provider_config["profile"] = args.aws_profile - updated = True - if not updated: - return {"error": True, "message": "No updates specified.", "exit_code": 1} + if spec is None: + return { + "error": True, + "message": f"Unknown provider type '{provider_type}'. No CLI spec registered.", + "exit_code": 1, + } + partial = spec.extract_partial_config(args) + if not partial: + return {"error": True, "message": "No updates specified.", "exit_code": 1} + provider_config.update(partial) # Test updated credentials success, error = _test_provider_credentials(provider_type, provider_config) diff --git a/src/orb/providers/README.md b/src/orb/providers/README.md index c392c0ac6..a6dd81b8c 100644 --- a/src/orb/providers/README.md +++ b/src/orb/providers/README.md @@ -11,7 +11,7 @@ The provider layer contains cloud-specific implementations that extend the core When accessing provider extension configurations, always use the `TemplateExtensionRegistry`: ```python -from src.domain.template.extensions import TemplateExtensionRegistry +from orb.infrastructure.registry.template_extension_registry import TemplateExtensionRegistry # Get extension config with schema defaults extension_config = TemplateExtensionRegistry.create_extension_config( @@ -518,6 +518,12 @@ class NewCloudProvider(ProviderInterface): - Provide clear extension examples - Support community contributions +## Developer Guide + +For a step-by-step walkthrough of adding a new provider — including the mandatory glue points, all extension point registries, the `OperationOutcome` contract, anti-patterns to avoid, and the recommended test layout — see: + +- [Adding a Provider](../../docs/root/developer_guide/adding_a_provider.md) + --- This provider layer enables seamless multi-cloud support while maintaining the clean architecture and cloud-agnostic design of the core system. Built on open standards and designed for extensibility, it provides a solid foundation for supporting any cloud platform. diff --git a/src/orb/providers/aws/auth/cognito_strategy.py b/src/orb/providers/aws/auth/cognito_strategy.py index 49e59e67b..01c7aa345 100644 --- a/src/orb/providers/aws/auth/cognito_strategy.py +++ b/src/orb/providers/aws/auth/cognito_strategy.py @@ -1,11 +1,18 @@ """AWS Cognito authentication strategy.""" -from typing import Optional +from __future__ import annotations + +import asyncio +import time +from base64 import urlsafe_b64decode +from typing import TYPE_CHECKING, Any, Optional import boto3 import jwt +import requests from botocore.config import Config from botocore.exceptions import ClientError +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers _DEFAULT_CONFIG = Config( connect_timeout=10, @@ -22,11 +29,19 @@ AuthStatus, ) +if TYPE_CHECKING: + pass + @injectable class CognitoAuthStrategy(AuthPort): """Authentication strategy using AWS Cognito User Pools.""" + # Class-level JWKS cache: maps jwks_url → {"fetched_at": float, "keys": list[dict]} + # Shared across all instances so pool restarts benefit from warm entries. + _jwks_cache: dict[str, dict[str, Any]] = {} + _cache_ttl_seconds: int = 3600 + def __init__( self, logger: LoggingPort, @@ -228,6 +243,54 @@ async def revoke_token(self, token: str) -> bool: self._logger.error("Token revocation error: %s", e) return False + @classmethod + def from_auth_config(cls, auth_config: Any) -> CognitoAuthStrategy: + """ + Build strategy instance from AuthConfig. + + Extracts the provider_auth.cognito sub-config and constructs a + CognitoAuthStrategy. A LoggingPort is obtained from the DI container + when available; otherwise a plain logging adapter is used. + + Args: + auth_config: AuthConfig instance with optional provider_auth.cognito sub-config + + Returns: + Configured CognitoAuthStrategy + """ + from orb.infrastructure.adapters.logging_adapter import LoggingAdapter + + provider_auth = getattr(auth_config, "provider_auth", None) + cognito_cfg = getattr(provider_auth, "cognito", None) if provider_auth is not None else None + + user_pool_id: str = ( + getattr(cognito_cfg, "user_pool_id", "") if cognito_cfg is not None else "" + ) + client_id: str = getattr(cognito_cfg, "client_id", "") if cognito_cfg is not None else "" + region: str = ( + getattr(cognito_cfg, "region", "us-east-1") if cognito_cfg is not None else "us-east-1" + ) + jwks_url: Optional[str] = ( + getattr(cognito_cfg, "jwks_url", None) if cognito_cfg is not None else None + ) + + try: + from orb.domain.base.ports import LoggingPort + from orb.infrastructure.di.container import get_container + + logger: LoggingPort = get_container().get(LoggingPort) + except Exception: + logger = LoggingAdapter() # type: ignore[assignment] + + return cls( + logger=logger, + user_pool_id=user_pool_id, + client_id=client_id, + region=region, + jwks_url=jwks_url, + enabled=True, + ) + def get_strategy_name(self) -> str: """ Get strategy name. @@ -246,37 +309,80 @@ def is_enabled(self) -> bool: """ return self.enabled - async def _get_public_key(self, kid: str) -> Optional[str]: + @staticmethod + def _b64url_to_int(val: str) -> int: + """Decode a base64url-encoded string to an integer (used for RSA key fields).""" + padded = val + "=" * (-len(val) % 4) + return int.from_bytes(urlsafe_b64decode(padded), "big") + + async def _get_public_key(self, kid: str) -> Any: """ Get public key from Cognito JWKS endpoint. + Fetches the JWKS (using the class-level cache; re-fetches after TTL), + locates the entry matching ``kid``, and converts it to a ``cryptography`` + RSAPublicKey object suitable for use with PyJWT. + + The synchronous ``requests.get`` call is offloaded via + ``asyncio.to_thread`` so the event loop is not blocked during the fetch. + + On ``jwt.InvalidTokenError`` (non-RSA key) the cache entry is evicted so + that a key rotation does not lock out valid tokens indefinitely. + Args: kid: Key ID from token header Returns: - Public key for token verification + RSAPublicKey for token verification, or None if not found + + Raises: + jwt.InvalidTokenError: If the matched key is not an RSA key """ try: - # In production, you would cache JWKS and implement appropriate key rotation - # This is a simplified implementation - import requests - - # Add timeout to prevent hanging connections (security best practice) - response = requests.get(self.jwks_url, timeout=30) - jwks = response.json() + jwks = await self._fetch_jwks_cached() for key in jwks.get("keys", []): if key.get("kid") == kid: - # Convert JWK to PEM format (simplified) - # In production, use a appropriate JWK library - return key # Return the key dict for now + if key.get("kty") != "RSA": + # Evict the cache entry so a subsequent rotation is picked up. + self._jwks_cache.pop(self.jwks_url, None) + raise jwt.InvalidTokenError( + f"Unsupported key type: {key.get('kty')!r}. Only RSA keys are supported." + ) + n = self._b64url_to_int(key["n"]) + e = self._b64url_to_int(key["e"]) + return RSAPublicNumbers(e=e, n=n).public_key() return None + except jwt.InvalidTokenError: + raise except Exception as e: self._logger.error("Failed to get public key: %s", e) return None + async def _fetch_jwks_cached(self) -> dict[str, Any]: + """Return the JWKS document for this strategy's endpoint. + + Serves from the class-level cache when the entry is younger than + ``_cache_ttl_seconds``; otherwise fetches fresh via + ``asyncio.to_thread`` and repopulates the cache. + """ + cached = self._jwks_cache.get(self.jwks_url) + if cached is not None: + age = time.monotonic() - cached["fetched_at"] + if age < self._cache_ttl_seconds: + return cached["jwks"] # type: ignore[return-value] + + def _do_fetch() -> dict[str, Any]: + response = requests.get(self.jwks_url, timeout=30) + response.raise_for_status() + return response.json() # type: ignore[no-any-return] + + jwks: dict[str, Any] = await asyncio.to_thread(_do_fetch) + self._jwks_cache[self.jwks_url] = {"fetched_at": time.monotonic(), "jwks": jwks} + return jwks + def _map_groups_to_roles(self, groups: list[str]) -> list[str]: """ Map Cognito groups to application roles. diff --git a/src/orb/providers/aws/auth/iam_strategy.py b/src/orb/providers/aws/auth/iam_strategy.py index cce79918e..d3bdc80ab 100644 --- a/src/orb/providers/aws/auth/iam_strategy.py +++ b/src/orb/providers/aws/auth/iam_strategy.py @@ -1,6 +1,9 @@ """AWS IAM authentication strategy.""" -from typing import Any, Optional +from __future__ import annotations + +import os +from typing import TYPE_CHECKING, Any, Optional from botocore.config import Config from botocore.exceptions import ClientError, NoCredentialsError @@ -15,6 +18,9 @@ ) from orb.providers.aws.session_factory import AWSSessionFactory +if TYPE_CHECKING: + pass + _DEFAULT_CONFIG = Config( connect_timeout=10, read_timeout=30, @@ -28,8 +34,6 @@ class IAMAuthStrategy(AuthPort): _DEFAULT_ADMIN_ROLE_PATTERNS: frozenset[str] = frozenset({"Admin", "Administrator", "OrbAdmin"}) - ASSUME_ALL_PERMISSIONS: bool = False - def __init__( self, logger: LoggingPort, @@ -62,7 +66,32 @@ def __init__( "ec2:TerminateInstances", ] self.enabled = enabled - self._assume_permissions = assume_permissions + + # assume_permissions is a dev-only escape hatch. It is only honoured when + # the operator has explicitly set ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY=true + # in the environment. Without that env-var the flag is silently disabled + # so that a misconfigured production deployment cannot accidentally bypass + # real IAM evaluation. + _dev_env_var = os.environ.get("ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY", "").lower() + _dev_override_active = _dev_env_var == "true" + + if assume_permissions and not _dev_override_active: + self._logger.critical( + "IAM assume_permissions=True is set in config but " + "ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY env var is not 'true'. " + "Treating as assume_permissions=False to prevent privilege bypass in production. " + "Set ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY=true only in non-production environments." + ) + self._assume_permissions = False + else: + self._assume_permissions = assume_permissions + + if self._assume_permissions: + self._logger.critical( + "IAM assume_permissions=True is ACTIVE (ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY=true). " + "All required_actions are granted without AWS evaluation. " + "This MUST NOT be used in production." + ) if not self._assume_permissions: self._logger.error( @@ -184,6 +213,54 @@ async def revoke_token(self, token: str) -> bool: """ return True + @classmethod + def from_auth_config(cls, auth_config: Any) -> IAMAuthStrategy: + """ + Build strategy instance from AuthConfig. + + Extracts the provider_auth.iam sub-config and constructs an IAMAuthStrategy. + A LoggingPort is obtained from the DI container when available; otherwise a + plain logging adapter is used so the classmethod stays self-contained. + + Args: + auth_config: AuthConfig instance with optional provider_auth.iam sub-config + + Returns: + Configured IAMAuthStrategy + """ + from orb.infrastructure.adapters.logging_adapter import LoggingAdapter + + provider_auth = getattr(auth_config, "provider_auth", None) + iam_cfg = getattr(provider_auth, "iam", None) if provider_auth is not None else None + + region: str = ( + getattr(iam_cfg, "region", "us-east-1") if iam_cfg is not None else "us-east-1" + ) + profile: Optional[str] = getattr(iam_cfg, "profile", None) if iam_cfg is not None else None + required_actions: list[str] = ( + getattr(iam_cfg, "required_actions", []) if iam_cfg is not None else [] + ) + assume_permissions: bool = ( + getattr(iam_cfg, "assume_permissions", False) if iam_cfg is not None else False + ) + + try: + from orb.domain.base.ports import LoggingPort + from orb.infrastructure.di.container import get_container + + logger: LoggingPort = get_container().get(LoggingPort) + except Exception: + logger = LoggingAdapter() # type: ignore[assignment] + + return cls( + logger=logger, + region=region, + profile=profile, + required_actions=required_actions, + enabled=True, + assume_permissions=assume_permissions, + ) + def get_strategy_name(self) -> str: """ Get strategy name. diff --git a/src/orb/providers/aws/defaults_loader.py b/src/orb/providers/aws/defaults_loader.py new file mode 100644 index 000000000..420f865fa --- /dev/null +++ b/src/orb/providers/aws/defaults_loader.py @@ -0,0 +1,37 @@ +"""AWS provider defaults loader.""" + +from __future__ import annotations + +import json + +from orb.domain.base.ports.provider_defaults_loader_port import ProviderDefaultsLoaderPort + + +class AWSDefaultsLoader: + """Loads defaults from the bundled ``aws_defaults.json`` config file. + + Satisfies :class:`~orb.domain.base.ports.provider_defaults_loader_port.ProviderDefaultsLoaderPort`. + """ + + def load_defaults(self) -> dict: + """Return AWS provider defaults from the bundled ``aws_defaults.json``. + + Returns: + Raw configuration dictionary contributed by the AWS provider. + Returns an empty dict if the file cannot be read. + """ + try: + from importlib.resources import files + + text = ( + files("orb.providers.aws.config") + .joinpath("aws_defaults.json") + .read_text(encoding="utf-8") + ) + return json.loads(text) + except Exception: + return {} + + +# Runtime check that AWSDefaultsLoader satisfies the protocol +assert isinstance(AWSDefaultsLoader(), ProviderDefaultsLoaderPort) diff --git a/src/orb/providers/aws/domain/template/aws_template_aggregate.py b/src/orb/providers/aws/domain/template/aws_template_aggregate.py index c10996414..f628d9e2f 100644 --- a/src/orb/providers/aws/domain/template/aws_template_aggregate.py +++ b/src/orb/providers/aws/domain/template/aws_template_aggregate.py @@ -14,7 +14,6 @@ from orb.domain.template.template_aggregate import Template from orb.providers.aws.domain.template.value_objects import ( - AWSAllocationStrategy, AWSConfiguration, AWSFleetType, AWSInstanceType, @@ -22,8 +21,8 @@ AWSSubnetId, AWSTags, ProviderApi, - normalise_allocation_strategy, ) +from orb.providers.aws.value_objects import AWSAllocationStrategy, normalise_allocation_strategy class AWSOptionalIntegerRange(BaseModel): @@ -203,6 +202,11 @@ class AWSTemplate(Template): model_config = ConfigDict(arbitrary_types_allowed=True) + # Typed provider-config forwarded from TemplateDTO (round-trip via model_dump). + # Accepts a dict (serialised AWSTemplateDTOConfig) or None; values are promoted + # to their respective fields in validate_aws_template before use. + provider_config: Optional[dict[str, Any]] = None + # AWS-specific fields provider_api: Optional[ProviderApi] = None # type: ignore[assignment] fleet_type: Optional[AWSFleetType] = None @@ -253,6 +257,38 @@ def validate_aws_template(self) -> "AWSTemplate": # (generic/example templates may have empty subnet_ids/image_id, filled at runtime # from provider.template_defaults via _coalesce_merge) + # Promote AWS-specific fields from provider_config dict (DTO round-trip path). + # provider_config is injected by TemplateDTO.from_domain via AWSTemplateDTOConfig. + _pc = getattr(self, "provider_config", None) + if isinstance(_pc, dict): + if not self.fleet_type: + _ft = _pc.get("fleet_type") + if _ft: + try: + object.__setattr__(self, "fleet_type", AWSFleetType(str(_ft).lower())) + except (ValueError, TypeError): + pass + if not self.fleet_role: + _fr = _pc.get("fleet_role") + if _fr: + object.__setattr__(self, "fleet_role", _fr) + if self.percent_on_demand is None: + _pod = _pc.get("percent_on_demand") + if _pod is not None: + object.__setattr__(self, "percent_on_demand", int(_pod)) + if not self.launch_template_id: + _ltid = _pc.get("launch_template_id") + if _ltid: + object.__setattr__(self, "launch_template_id", _ltid) + if self.abis_instance_requirements is None: + _abis = _pc.get("abis_instance_requirements") + if _abis is not None: + object.__setattr__( + self, + "abis_instance_requirements", + ABISInstanceRequirements.model_validate(_abis), + ) + # Auto-assign default fleet_type if not provided # Set fleet_type from metadata if not already set if not self.fleet_type: diff --git a/src/orb/providers/aws/domain/template/aws_template_dto_config.py b/src/orb/providers/aws/domain/template/aws_template_dto_config.py new file mode 100644 index 000000000..410942b66 --- /dev/null +++ b/src/orb/providers/aws/domain/template/aws_template_dto_config.py @@ -0,0 +1,39 @@ +"""AWS-specific typed DTO configuration for TemplateDTO serialisation.""" + +from typing import Any, Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class AWSTemplateDTOConfig(BaseModel): + model_config = ConfigDict(extra="ignore") + """Typed container for AWS-specific fields on TemplateDTO. + + This class holds only the fields that are AWS-specific and were + previously scattered between the top-level TemplateDTO attributes + and the opaque ``metadata`` dict. It is registered with + ``TemplateExtensionRegistry`` so that ``TemplateDTO.from_domain`` + can delegate construction to the registry rather than containing + AWS knowledge directly. + """ + + # EC2 Fleet / Spot Fleet configuration + fleet_type: Optional[str] = Field(None, description="Fleet type (maintain, request, instant)") + fleet_role: Optional[str] = Field(None, description="IAM role ARN for Spot Fleet") + percent_on_demand: Optional[int] = Field( + None, description="Percentage of On-Demand capacity in a heterogeneous fleet" + ) + + # Launch template reference + launch_template_id: Optional[str] = Field( + None, description="EC2 Launch Template ID to use instead of inline spec" + ) + + # Attribute-based instance selection payload (already serialised to dict) + abis_instance_requirements: Optional[dict[str, Any]] = Field( + None, description="InstanceRequirements dict for attribute-based instance selection" + ) + + def to_template_defaults(self) -> dict[str, Any]: + """Return a flat dict of non-None values suitable for template defaults merging.""" + return {k: v for k, v in self.model_dump().items() if v is not None} diff --git a/src/orb/providers/aws/exceptions/aws_exceptions.py b/src/orb/providers/aws/exceptions/aws_exceptions.py index 3adceed9e..6486bf3be 100644 --- a/src/orb/providers/aws/exceptions/aws_exceptions.py +++ b/src/orb/providers/aws/exceptions/aws_exceptions.py @@ -16,22 +16,42 @@ def __init__( message: str, details: Optional[dict[str, Any]] = None, error_code: Optional[str] = None, + aws_error_code: Optional[str] = None, + aws_error_message: Optional[str] = None, + aws_request_id: Optional[str] = None, + error_source: Optional[str] = None, ) -> None: """Initialize AWS exception. Args: message: Human-readable error message details: Additional error details and context - error_code: Specific error code for programmatic handling + error_code: Domain-level error code for programmatic handling + aws_error_code: Original AWS API error code (e.g. UnauthorizedOperation) + aws_error_message: Original AWS API error message + aws_request_id: AWS request ID for Support cases + error_source: AWS service.operation that failed (e.g. aws.ec2.RunInstances) """ super().__init__(message, error_code or self.__class__.__name__, details) self.error_code = error_code or self.__class__.__name__ + self.aws_error_code = aws_error_code + self.aws_error_message = aws_error_message + self.aws_request_id = aws_request_id + self.error_source = error_source def to_dict(self) -> dict[str, Any]: """Convert error to dictionary.""" result: dict[str, Any] = super().to_dict() # type: ignore[attr-defined] if self.error_code and self.error_code != self.__class__.__name__: result["error_code"] = self.error_code + if self.aws_error_code: + result["aws_error_code"] = self.aws_error_code + if self.aws_error_message: + result["aws_error_message"] = self.aws_error_message + if self.aws_request_id: + result["aws_request_id"] = self.aws_request_id + if self.error_source: + result["error_source"] = self.error_source return result diff --git a/src/orb/providers/aws/health.py b/src/orb/providers/aws/health.py index c4c57ddea..899da488a 100644 --- a/src/orb/providers/aws/health.py +++ b/src/orb/providers/aws/health.py @@ -12,12 +12,25 @@ from orb.providers.aws.infrastructure.aws_client import AWSClient -def register_aws_health_checks(health_check: HealthCheckPort, aws_client: "AWSClient") -> None: +def register_aws_health_checks( + health_check: HealthCheckPort, + aws_client: "AWSClient", + storage_strategy: str = "json", +) -> None: """Register AWS-specific health checks with the given HealthCheck instance. + The ``aws`` (STS) and ``ec2`` checks are always registered because they + validate core AWS connectivity. The ``dynamodb`` check is only registered + when the configured storage backend is ``"dynamodb"``; registering it + unconditionally causes ``/health`` to return 503 when the default + file/JSON storage is in use because the ``dynamodb:ListTables`` call + fails with no credentials or permissions. + Args: health_check: The application HealthCheckPort to register checks on. aws_client: Authenticated AWS client used by the checks. + storage_strategy: The configured storage backend (e.g. ``"json"``, + ``"sql"``, ``"dynamodb"``). Defaults to ``"json"``. """ def _check_aws_health() -> HealthStatus: @@ -85,4 +98,5 @@ def _check_dynamodb_health() -> HealthStatus: health_check.register_check("aws", _check_aws_health) health_check.register_check("ec2", _check_ec2_health) - health_check.register_check("dynamodb", _check_dynamodb_health) + if storage_strategy == "dynamodb": + health_check.register_check("dynamodb", _check_dynamodb_health) diff --git a/src/orb/providers/aws/infrastructure/handlers/asg/capacity_manager.py b/src/orb/providers/aws/infrastructure/handlers/asg/capacity_manager.py index 367ec30ed..52cc41d9a 100644 --- a/src/orb/providers/aws/infrastructure/handlers/asg/capacity_manager.py +++ b/src/orb/providers/aws/infrastructure/handlers/asg/capacity_manager.py @@ -5,8 +5,10 @@ 1. Pre-termination capacity reduction — lower DesiredCapacity and MinSize so AWS does not replace the instances we are about to terminate. -2. Instance detachment with capacity decrement — detach specific instances from - a known ASG, lower MinSize if needed, then terminate. +2. Instance termination with weight-aware capacity decrement — call + terminate_instance_in_auto_scaling_group per instance so AWS decrements + DesiredCapacity by the instance's WeightedCapacity (not by 1), which is + symmetric with scale-up behaviour in Mixed Instance Policy ASGs. """ from typing import Any, Callable, Optional @@ -128,11 +130,13 @@ def reduce_capacity(self, instance_ids: list[str]) -> None: def release_instances( self, asg_name: str, instance_ids: list[str], asg_details: dict[str, Any] ) -> None: - """Detach instances from an ASG, adjust MinSize if needed, then terminate. + """Terminate instances via the ASG API, adjusting MinSize if needed. + Uses terminate_instance_in_auto_scaling_group so DesiredCapacity is + decremented by WeightedCapacity (weight-aware, unlike detach_instances). If asg_details is empty the instances are terminated directly without attempting ASG-specific operations. When DesiredCapacity reaches zero - after detachment the ASG and its associated launch template are deleted. + the ASG and its associated launch template are deleted. """ self._logger.info("Processing ASG %s with %s instances", asg_name, len(instance_ids)) @@ -172,21 +176,61 @@ def release_instances( self._cleanup_on_zero_capacity("asg", asg_name) return - # Detach instances (API limit: 50 per call; use 20 for safety) - for chunk in self._chunk_list(instance_ids, 20): + # Guard against double-execution: only terminate instances that are currently + # members of this ASG. If release_instances is called a second time for the + # same set (e.g. due to an IN_PROGRESS status retry or a race), the instances + # are already gone and this check prevents a second DesiredCapacity decrement. + instances_to_terminate = self._filter_asg_members(asg_name, instance_ids) + if not instances_to_terminate: + self._logger.info( + "ASG %s: all %d instance(s) already terminated/detached — skipping ASG termination", + asg_name, + len(instance_ids), + ) + # Instances may still be running (standalone after a prior partial operation); + # terminate them directly so the return request can complete. + self._aws_ops.terminate_instances_with_fallback( + instance_ids, self._request_adapter, f"ASG {asg_name} instances (already detached)" + ) + return + + skipped = [i for i in instance_ids if i not in instances_to_terminate] + if skipped: + self._logger.info( + "ASG %s: %d instance(s) already terminated/detached (skipping): %s", + asg_name, + len(skipped), + skipped, + ) + + # Use terminate_instance_in_auto_scaling_group (one instance per API call). + # Unlike detach_instances — which decrements DesiredCapacity by the *count* + # of instances (always 1 per instance regardless of weight) — this API + # decrements DesiredCapacity by the instance's WeightedCapacity, which is + # symmetric with scale-up in Mixed Instance Policy ASGs. It also handles + # both the termination and the capacity decrement atomically, so no + # separate EC2 terminate step is needed. + for instance_id in instances_to_terminate: self._retry_with_backoff( - self._aws_client.autoscaling_client.detach_instances, + self._aws_client.autoscaling_client.terminate_instance_in_auto_scaling_group, operation_type="critical", - AutoScalingGroupName=asg_name, - InstanceIds=chunk, + InstanceId=instance_id, ShouldDecrementDesiredCapacity=True, ) - self._logger.debug("Detached chunk from ASG %s: %s", asg_name, chunk) - self._logger.info("Detached instances from ASG %s: %s", asg_name, instance_ids) + self._logger.debug( + "Terminated instance %s in ASG %s with weight-aware capacity decrement", + instance_id, + asg_name, + ) + self._logger.info( + "Terminated instances in ASG %s (weight-aware): %s", asg_name, instances_to_terminate + ) - # Re-describe the ASG to get the live DesiredCapacity after detach, since - # ShouldDecrementDesiredCapacity=True may have already decremented it in AWS - # and the value in asg_details is now stale. + # Re-describe the ASG to get live state after termination. + # terminate_instance_in_auto_scaling_group decrements DesiredCapacity by + # WeightedCapacity, so DesiredCapacity == 0 reliably signals an empty fleet + # for both unweighted and weighted ASGs. No secondary instance-list check needed. + live_desired = 0 try: live_response = self._retry_with_backoff( self._aws_client.autoscaling_client.describe_auto_scaling_groups, @@ -194,14 +238,15 @@ def release_instances( AutoScalingGroupNames=[asg_name], ) live_groups = live_response.get("AutoScalingGroups", []) - live_desired = live_groups[0].get("DesiredCapacity", 0) if live_groups else 0 + if live_groups: + live_desired = live_groups[0].get("DesiredCapacity", 0) or 0 except Exception as exc: self._logger.warning( - "Failed to re-describe ASG %s after detach, falling back to computed capacity: %s", + "Failed to re-describe ASG %s after termination, falling back to computed capacity: %s", asg_name, exc, ) - live_desired = max(0, asg_details["DesiredCapacity"] - len(instance_ids)) + live_desired = max(0, asg_details["DesiredCapacity"] - len(instances_to_terminate)) new_capacity = max(0, live_desired) @@ -214,13 +259,8 @@ def release_instances( ) self._logger.info("Reduced ASG %s MinSize to %s", asg_name, new_capacity) - self._aws_ops.terminate_instances_with_fallback( - instance_ids, self._request_adapter, f"ASG {asg_name} instances" - ) - self._logger.info("Terminated ASG %s instances: %s", asg_name, instance_ids) - if new_capacity == 0: - self._logger.info("ASG %s capacity is zero, deleting ASG", asg_name) + self._logger.info("ASG %s is empty, deleting ASG", asg_name) self._call_delete_asg(asg_name) self._cleanup_on_zero_capacity("asg", asg_name) @@ -228,6 +268,66 @@ def release_instances( # Private helpers # ------------------------------------------------------------------ + def _filter_asg_members(self, asg_name: str, instance_ids: list[str]) -> list[str]: + """Return the subset of instance_ids that are currently attached to asg_name. + + Used as an idempotency guard in release_instances: if an instance has already + been terminated or detached (e.g. by a prior call), it is excluded so that + ShouldDecrementDesiredCapacity=True is not applied a second time. + """ + if not instance_ids: + return [] + try: + response = self._retry_with_backoff( + self._aws_client.autoscaling_client.describe_auto_scaling_instances, + operation_type="read_only", + InstanceIds=instance_ids, + ) + entries = response.get("AutoScalingInstances", []) + # If describe returned no entries, the API call did not give us + # actionable membership info — fall back to processing all + # instances rather than silently dropping them. This matches the + # exception-path behaviour below. + if not entries: + return list(instance_ids) + # Only terminate instances that are currently in a state where + # ShouldDecrementDesiredCapacity=True is meaningful. Instances in + # Detaching / Detached / Terminated already had their DesiredCapacity + # decremented on the first call; including them again would double-count. + _DETACHABLE_STATES = {"InService", "Standby"} + + # Map: instance_id → lifecycle_state for every entry belonging to asg_name. + state_by_id = { + entry["InstanceId"]: entry.get("LifecycleState", "") + for entry in entries + if entry.get("AutoScalingGroupName") == asg_name + } + + if not state_by_id: + # None of the requested instances appeared in describe output at all. + # Could mean: (a) instances were never in this ASG, (b) describe had a + # gap. Fall back to processing all instances to be safe — the original + # guard logic below will fall through to terminate_instances_with_fallback. + return list(instance_ids) + + # Return only the instances in a detachable lifecycle state. + filtered = [iid for iid in instance_ids if state_by_id.get(iid) in _DETACHABLE_STATES] + # instances not in state_by_id are not (or no longer) in this ASG; + # they can be skipped (no longer need weight-aware termination). + return filtered + except Exception as exc: + self._logger.warning( + "Failed to verify ASG %s membership for instances %s; " + "proceeding with all instances to avoid leaving them running: %s", + asg_name, + instance_ids, + exc, + ) + # On error, be conservative: attempt termination for all instances. + # A "not a member" error from terminate_instance_in_auto_scaling_group + # is better than skipping a needed capacity decrement. + return list(instance_ids) + def _call_delete_asg(self, asg_name: str) -> None: """Invoke the registered delete-ASG callback, or fall back to direct deletion.""" if self._delete_asg_fn is not None: diff --git a/src/orb/providers/aws/infrastructure/handlers/asg/handler.py b/src/orb/providers/aws/infrastructure/handlers/asg/handler.py index 26b3aae51..f147d3bd6 100644 --- a/src/orb/providers/aws/infrastructure/handlers/asg/handler.py +++ b/src/orb/providers/aws/infrastructure/handlers/asg/handler.py @@ -33,6 +33,7 @@ from orb.domain.base.dependency_injection import injectable from orb.domain.base.ports import LoggingPort from orb.domain.base.ports.configuration_port import ConfigurationPort +from orb.domain.base.provider_fulfilment import CheckHostsStatusResult, ProviderFulfilment from orb.domain.request.aggregate import Request from orb.domain.template.template_aggregate import Template from orb.infrastructure.adapters.ports.request_adapter_port import RequestAdapterPort @@ -520,36 +521,235 @@ def _group_details_key(self) -> str: def _grouping_label(self) -> str: return "ASG" - def check_hosts_status(self, request: Request) -> list[dict[str, Any]]: - """Check the status of instances across all ASGs in the request.""" + def check_hosts_status(self, request: Request) -> CheckHostsStatusResult: + """Check the status of instances across all ASGs in the request. + + Fulfilment semantics: + sum(WeightedCapacity for InService instances) >= DesiredCapacity + AND pending_count == 0 AND failed_count == 0 → fulfilled. + + For ASGs without WeightedCapacity (uniform instance types) each + InService instance contributes 1 unit. + """ try: if not request.resource_ids: self._logger.info("No ASG names found in request") - return [] + return CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message="No ASG names yet — waiting for provisioning", + target_units=request.requested_count, + running_count=0, + pending_count=0, + failed_count=0, + ), + ) - all_instances = [] + all_instances: list[dict[str, Any]] = [] + asg_results: list[CheckHostsStatusResult] = [] - # Process all ASG names instead of just the first one for asg_name in request.resource_ids: try: - asg_instances = self._get_asg_instances( + result = self._get_asg_status( asg_name, request_id=str(request.request_id), - resource_id=asg_name, + requested_count=request.requested_count, ) - if asg_instances: - formatted_instances = self._format_instance_data( - asg_instances, asg_name, self._resolve_provider_api(request) - ) - all_instances.extend(formatted_instances) + all_instances.extend(result.instances) + asg_results.append(result) except Exception as e: self._logger.error("Failed to get instances for ASG %s: %s", asg_name, e) continue - return all_instances + + if not asg_results: + return CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message="No ASG status available — will retry", + ), + ) + + if len(asg_results) == 1: + return CheckHostsStatusResult( + instances=all_instances, + fulfilment=asg_results[0].fulfilment, + ) + + states = [r.fulfilment.state for r in asg_results] + if all(s == "fulfilled" for s in states): + combined_state = "fulfilled" + combined_msg = f"All {len(asg_results)} ASGs fulfilled" + elif any(s == "failed" for s in states): + combined_state = "failed" + combined_msg = "One or more ASGs failed" + elif any(s == "partial" for s in states): + combined_state = "partial" + combined_msg = "One or more ASGs partially fulfilled" + else: + combined_state = "in_progress" + combined_msg = "Waiting for ASG(s) to fulfil" + + return CheckHostsStatusResult( + instances=all_instances, + fulfilment=ProviderFulfilment(state=combined_state, message=combined_msg), + ) + except Exception as e: self._logger.error("Unexpected error checking ASG status: %s", str(e)) raise AWSInfrastructureError(f"Failed to check ASG status: {e!s}") + def _get_asg_status( + self, + asg_name: str, + request_id: str = "", + requested_count: int = 1, + ) -> CheckHostsStatusResult: + """Get status + fulfilment for a specific ASG.""" + response = self._retry_with_backoff( + self.aws_client.autoscaling_client.describe_auto_scaling_groups, + operation_type="standard", + AutoScalingGroupNames=[asg_name], + ) + groups = response.get("AutoScalingGroups", []) + if not groups: + self._logger.warning("ASG %s not found", asg_name) + return CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message=f"ASG {asg_name} not yet visible — waiting", + target_units=requested_count, + running_count=0, + pending_count=0, + failed_count=0, + ), + ) + + group = groups[0] + desired_capacity: int = group.get("DesiredCapacity") or 0 + raw_instances = group.get("Instances") or [] + + # Compute weighted fulfilment from ASG Instances list + in_service_weighted = sum( + int(inst.get("WeightedCapacity") or 1) + for inst in raw_instances + if inst.get("LifecycleState") == "InService" + ) + pending_raw = [ + inst + for inst in raw_instances + if inst.get("LifecycleState") in ("Pending", "Pending:Wait", "Pending:Proceed") + ] + pending_count = len(pending_raw) + in_service_count = sum( + 1 for inst in raw_instances if inst.get("LifecycleState") == "InService" + ) + + # Get full EC2 instance details for instances in the ASG + instance_ids = [ + inst["InstanceId"] + for inst in raw_instances + if inst.get("InstanceId") + and inst.get("LifecycleState") + not in ("Terminating", "Terminated", "Detaching", "Detached") + ] + + if not instance_ids: + return CheckHostsStatusResult( + instances=[], + fulfilment=self._compute_asg_fulfilment( + desired_capacity=desired_capacity, + in_service_weighted=in_service_weighted, + pending_count=pending_count, + in_service_count=in_service_count, + ec2_instances=[], + ), + ) + + try: + instance_details = self._get_instance_details( + instance_ids, + request_id=request_id, + resource_id=asg_name, + provider_api="ASG", + ) + except Exception as e: + self._logger.warning("Failed to describe EC2 instances for ASG %s: %s", asg_name, e) + instance_details = [] + + provider_api_value = (getattr(self, "metadata", {}) or {}).get("provider_api", "ASG") + formatted = ( + self._format_instance_data(instance_details, asg_name, provider_api_value) + if instance_details + else [] + ) + + fulfilment = self._compute_asg_fulfilment( + desired_capacity=desired_capacity, + in_service_weighted=in_service_weighted, + pending_count=pending_count, + in_service_count=in_service_count, + ec2_instances=formatted, + ) + return CheckHostsStatusResult(instances=formatted, fulfilment=fulfilment) + + def _compute_asg_fulfilment( + self, + desired_capacity: int, + in_service_weighted: int, + pending_count: int, + in_service_count: int, + ec2_instances: list[dict[str, Any]], + ) -> ProviderFulfilment: + """Compute ProviderFulfilment for an ASG request. + + sum(WeightedCapacity for InService) >= DesiredCapacity AND + pending_count == 0 AND failed_count == 0 → fulfilled. + """ + failed_count = sum(1 for i in ec2_instances if i.get("status") in ("failed", "error")) + running_count = sum(1 for i in ec2_instances if i.get("status") == "running") + + asg_fully_fulfilled = desired_capacity > 0 and in_service_weighted >= desired_capacity + + if asg_fully_fulfilled and pending_count == 0 and failed_count == 0: + return ProviderFulfilment( + state="fulfilled", + message=( + f"ASG fulfilled: {in_service_count} instance(s) InService " + f"({in_service_weighted}/{desired_capacity} weighted capacity)" + ), + target_units=desired_capacity, + fulfilled_units=in_service_weighted, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + elif failed_count > 0 and in_service_count == 0 and pending_count == 0: + return ProviderFulfilment( + state="failed", + message=f"ASG failed: {failed_count} instance(s) in failure state", + target_units=desired_capacity, + fulfilled_units=in_service_weighted, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + else: + return ProviderFulfilment( + state="in_progress", + message=( + f"ASG: {in_service_count} InService, {pending_count} pending " + f"({in_service_weighted}/{desired_capacity} weighted capacity)" + ), + target_units=desired_capacity, + fulfilled_units=in_service_weighted, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + @classmethod def get_example_templates(cls) -> list[Template]: """Get example templates for ASG handler.""" @@ -560,7 +760,7 @@ def get_example_templates(cls) -> list[Template]: description="Auto Scaling Group with on-demand instances only", provider_api="ASG", machine_types={"t3.medium": 2, "t3.xlarge": 4}, - max_instances=15, + max_instances=100, price_type="ondemand", subnet_ids=[], security_group_ids=[], @@ -572,7 +772,7 @@ def get_example_templates(cls) -> list[Template]: description="Auto Scaling Group with spot instances only", provider_api="ASG", machine_types={"t3.medium": 2, "t3.xlarge": 4}, - max_instances=20, + max_instances=100, price_type="spot", max_price=0.10, subnet_ids=[], @@ -585,7 +785,7 @@ def get_example_templates(cls) -> list[Template]: description="Auto Scaling Group with mixed on-demand and spot instances", provider_api="ASG", machine_types={"t3.medium": 2, "t3.large": 2, "t3.xlarge": 4}, - max_instances=25, + max_instances=100, price_type="heterogeneous", percent_on_demand=30, allocation_strategy="lowestPrice", diff --git a/src/orb/providers/aws/infrastructure/handlers/base_handler.py b/src/orb/providers/aws/infrastructure/handlers/base_handler.py index e0a8d0c9f..c9e74bc59 100644 --- a/src/orb/providers/aws/infrastructure/handlers/base_handler.py +++ b/src/orb/providers/aws/infrastructure/handlers/base_handler.py @@ -8,15 +8,19 @@ import asyncio import json +import re from abc import ABC, abstractmethod from datetime import datetime -from typing import Any, Callable, Optional, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar from botocore.exceptions import ClientError from orb.application.base.provider_handlers import BaseProviderHandler from orb.domain.base.dependency_injection import injectable from orb.domain.base.exceptions import InfrastructureError + +if TYPE_CHECKING: + from orb.domain.base.provider_fulfilment import CheckHostsStatusResult from orb.domain.base.ports import ErrorHandlingPort, LoggingPort from orb.domain.base.ports.configuration_port import ConfigurationPort from orb.domain.request.aggregate import Request @@ -165,15 +169,21 @@ def _acquire_hosts_internal( """ @abstractmethod - def check_hosts_status(self, request: Request) -> list[dict[str, Any]]: - """ - Check the status of hosts for a request. + def check_hosts_status(self, request: Request) -> "CheckHostsStatusResult": + """Check the status of hosts for a request. + + Returns a ``CheckHostsStatusResult`` containing both per-instance + detail dicts and the provider's ``ProviderFulfilment`` verdict. + + Every concrete handler MUST emit a ``ProviderFulfilment`` — no + fallback to count-based logic is permitted. The application layer + raises ``ProviderContractError`` if fulfilment is absent. Args: request: The request to check Returns: - List of instance details + CheckHostsStatusResult with instances and fulfilment verdict. Raises: AWSEntityNotFoundError: If the AWS resource is not found @@ -443,15 +453,41 @@ def _get_retry_strategy_config( "max_delay": 30.0, } + # Pattern matching AWS temporary credential access key prefixes — strip if + # accidentally included in an error message. + _ACCESS_KEY_RE = re.compile(r"A[KS]IA[A-Z0-9]{16}", re.ASCII) + + @classmethod + def _redact_aws_message(cls, message: str) -> str: + """Remove access-key-like tokens from an AWS error message.""" + return cls._ACCESS_KEY_RE.sub("[REDACTED]", message) + def _convert_client_error( self, error: ClientError, operation_name: str = "unknown" ) -> Exception: - """Convert AWS ClientError to domain exception.""" + """Convert AWS ClientError to domain exception, preserving AWS error details.""" error_code = error.response["Error"]["Code"] - error_message = error.response["Error"]["Message"] + raw_message = error.response["Error"]["Message"] + error_message = self._redact_aws_message(raw_message) + + # Derive the service name from the handler class for error_source attribution. + service_label = self._get_service_name() + error_source = f"aws.{service_label}.{operation_name}" + + # Extract the AWS request ID if present (useful for AWS Support cases). + response_metadata = error.response.get("ResponseMetadata", {}) + aws_request_id: str | None = response_metadata.get("RequestId") + + # Common kwargs carried into every domain exception subclass. + aws_kwargs: dict[str, Any] = { + "aws_error_code": error_code, + "aws_error_message": error_message, + "aws_request_id": aws_request_id, + "error_source": error_source, + } if error_code in ["ValidationError", "InvalidParameterValue"]: - return AWSValidationError(error_message) + return AWSValidationError(error_message, **aws_kwargs) elif error_code in [ "LimitExceeded", "InstanceLimitExceeded", @@ -460,19 +496,24 @@ def _convert_client_error( "ServiceQuotaExceededException", "ResourceCountExceeded", ]: - return QuotaExceededError(error_message) + return QuotaExceededError(error_message, **aws_kwargs) elif error_code == "ResourceInUse": - return ResourceInUseError(error_message) + return ResourceInUseError(error_message, **aws_kwargs) elif error_code in ["UnauthorizedOperation", "AccessDenied"]: - return AuthorizationError(error_message) + return AuthorizationError(error_message, **aws_kwargs) elif error_code == "RequestLimitExceeded": - return RateLimitError(error_message) + return RateLimitError(error_message, **aws_kwargs) elif error_code in ["ResourceNotFound", "InvalidInstanceID.NotFound"]: - return AWSEntityNotFoundError(error_message) + return AWSEntityNotFoundError(error_message, **aws_kwargs) elif error_code in ["RequestTimeout", "ServiceUnavailable"]: - return NetworkError(error_message) + return NetworkError(error_message, **aws_kwargs) else: - return InfrastructureError(f"AWS Error: {error_code} - {error_message}") + from orb.providers.aws.exceptions.aws_exceptions import AWSInfrastructureError + + return AWSInfrastructureError( + f"AWS Error: {error_code} - {error_message}", + **aws_kwargs, + ) def _paginate(self, client_method: Callable, result_key: str, **kwargs) -> list[dict[str, Any]]: """ diff --git a/src/orb/providers/aws/infrastructure/handlers/ec2_fleet/handler.py b/src/orb/providers/aws/infrastructure/handlers/ec2_fleet/handler.py index a7c0fbaeb..507e2bcd5 100644 --- a/src/orb/providers/aws/infrastructure/handlers/ec2_fleet/handler.py +++ b/src/orb/providers/aws/infrastructure/handlers/ec2_fleet/handler.py @@ -33,13 +33,14 @@ from orb.domain.base.dependency_injection import injectable from orb.domain.base.ports import LoggingPort from orb.domain.base.ports.configuration_port import ConfigurationPort +from orb.domain.base.provider_fulfilment import CheckHostsStatusResult, ProviderFulfilment from orb.domain.request.aggregate import Request from orb.domain.template.template_aggregate import Template from orb.infrastructure.adapters.ports.request_adapter_port import RequestAdapterPort from orb.infrastructure.error.decorators import handle_infrastructure_exceptions from orb.infrastructure.resilience import CircuitBreakerOpenError from orb.providers.aws.domain.template.aws_template_aggregate import AWSTemplate -from orb.providers.aws.domain.template.value_objects import AWSAllocationStrategy, AWSFleetType +from orb.providers.aws.domain.template.value_objects import AWSFleetType from orb.providers.aws.exceptions.aws_exceptions import ( AWSEntityNotFoundError, AWSInfrastructureError, @@ -53,11 +54,15 @@ EC2FleetReleaseManager, ) from orb.providers.aws.infrastructure.handlers.shared.base_context_mixin import BaseContextMixin +from orb.providers.aws.infrastructure.handlers.shared.fleet_fulfilment import ( + compute_capacity_based_fulfilment, +) from orb.providers.aws.infrastructure.handlers.shared.fleet_grouping_mixin import FleetGroupingMixin from orb.providers.aws.infrastructure.launch_template.manager import ( AWSLaunchTemplateManager, ) from orb.providers.aws.utilities.aws_operations import AWSOperations +from orb.providers.aws.value_objects import AWSAllocationStrategy @injectable @@ -153,7 +158,9 @@ def _acquire_hosts_internal( # lazily by the check_hosts_status / _check_single_fleet_status polling path. instance_ids = fleet_result.get("instance_ids", []) if instance_ids: - instances = [{"instance_id": iid} for iid in instance_ids] + instances = [ + {"instance_id": iid, "resource_id": fleet_id} for iid in instance_ids + ] self._logger.info( "EC2Fleet instant fleet created with %d instance(s): %s", len(instance_ids), @@ -372,24 +379,71 @@ def _create_fleet_config( lt_version=launch_template_version, ) - def check_hosts_status(self, request: Request) -> list[dict[str, Any]]: - """Check the status of instances in the fleet.""" + def check_hosts_status(self, request: Request) -> CheckHostsStatusResult: + """Check the status of instances in the fleet. + + Fulfilment semantics (per fleet type): + - Instant: same as RunInstances — running_count >= requested_count + and failed_count == 0 → fulfilled. ``fulfillment_final=True`` so + partial/failed can be detected when pending reaches zero. + - Maintain / Request: FulfilledCapacity >= TargetCapacity AND + pending_count == 0 AND failed_count == 0 → fulfilled. This is the + weighted-fleet path that fixes the live test timeout. + """ self._logger.debug(f" check_hosts_status {request}") if not request.resource_ids: raise AWSInfrastructureError("No Fleet ID found in request") - all_results: list[dict] = [] + all_instances: list[dict] = [] + # For multi-fleet requests collect instances; compute combined fulfilment at the end. + fleet_results: list[CheckHostsStatusResult] = [] for fleet_id in request.resource_ids: try: - results = self._check_single_fleet_status(fleet_id, request) - all_results.extend(results) + result = self._check_single_fleet_status(fleet_id, request) + all_instances.extend(result.instances) + fleet_results.append(result) except Exception as e: self._logger.warning( "Failed to check status for fleet %s, skipping: %s", fleet_id, e ) - return all_results - def _check_single_fleet_status(self, fleet_id: str, request: Request) -> list[dict]: + if not fleet_results: + return CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message="No fleet status available — will retry", + ), + ) + + # For a single fleet (typical case) return its result directly. + if len(fleet_results) == 1: + return CheckHostsStatusResult( + instances=all_instances, + fulfilment=fleet_results[0].fulfilment, + ) + + # Multiple fleets: aggregate — all must be fulfilled for overall fulfilled. + states = [r.fulfilment.state for r in fleet_results] + if all(s == "fulfilled" for s in states): + combined_state = "fulfilled" + combined_msg = f"All {len(fleet_results)} fleets fulfilled" + elif any(s == "failed" for s in states): + combined_state = "failed" + combined_msg = "One or more fleets failed" + elif any(s == "partial" for s in states): + combined_state = "partial" + combined_msg = "One or more fleets partially fulfilled" + else: + combined_state = "in_progress" + combined_msg = "Waiting for fleet(s) to fulfil" + + return CheckHostsStatusResult( + instances=all_instances, + fulfilment=ProviderFulfilment(state=combined_state, message=combined_msg), + ) + + def _check_single_fleet_status(self, fleet_id: str, request: Request) -> CheckHostsStatusResult: """Check the status of instances in a single fleet.""" try: fleet_type_value = request.metadata.get("fleet_type") @@ -434,11 +488,16 @@ def _check_single_fleet_status(self, fleet_id: str, request: Request) -> list[di self._logger.debug(f" check_hosts_status final fleet_type: {fleet_type}") + # Read capacity data from DescribeFleets (already called above — no extra API call) + spec = fleet.get("TargetCapacitySpecification") or {} + target_capacity = spec.get("TotalTargetCapacity") + fulfilled_capacity = fleet.get("FulfilledCapacity") or 0 + self._logger.debug( "Fleet status: %s, Target capacity: %s, Fulfilled capacity: %s", fleet.get("FleetState"), - fleet.get("TargetCapacitySpecification", {}).get("TotalTargetCapacity"), - fleet.get("FulfilledCapacity", 0), + target_capacity, + fulfilled_capacity, ) instance_ids = [] @@ -478,7 +537,14 @@ def _check_single_fleet_status(self, fleet_id: str, request: Request) -> list[di if not instance_ids: self._logger.info("No active instances found in fleet %s", fleet_id) - return [] + fulfilment = self._compute_ec2fleet_fulfilment( + fleet_type=fleet_type, + instances=[], + target_capacity=target_capacity, + fulfilled_capacity=fulfilled_capacity, + requested_count=request.requested_count, + ) + return CheckHostsStatusResult(instances=[], fulfilment=fulfilment) instance_details = self._get_instance_details( instance_ids, @@ -486,9 +552,17 @@ def _check_single_fleet_status(self, fleet_id: str, request: Request) -> list[di resource_id=fleet_id, provider_api="EC2Fleet", ) - return self._format_instance_data( + instances = self._format_instance_data( instance_details, fleet_id, self._resolve_provider_api(request) ) + fulfilment = self._compute_ec2fleet_fulfilment( + fleet_type=fleet_type, + instances=instances, + target_capacity=target_capacity, + fulfilled_capacity=fulfilled_capacity, + requested_count=request.requested_count, + ) + return CheckHostsStatusResult(instances=instances, fulfilment=fulfilment) except ClientError as e: error = self._convert_client_error(e) @@ -498,6 +572,90 @@ def _check_single_fleet_status(self, fleet_id: str, request: Request) -> list[di self._logger.error("Unexpected error checking EC2 Fleet status: %s", str(e)) raise AWSInfrastructureError(f"Failed to check EC2 Fleet status: {e!s}") + def _compute_ec2fleet_fulfilment( + self, + fleet_type: Optional[AWSFleetType], + instances: list[dict[str, Any]], + target_capacity: Optional[int], + fulfilled_capacity: float, + requested_count: int, + ) -> ProviderFulfilment: + """Compute ProviderFulfilment for an EC2 Fleet request. + + Instant fleets: running_count >= requested_count — same as RunInstances. + Maintain/Request fleets: FulfilledCapacity >= TargetCapacity AND + pending_count == 0 AND failed_count == 0. + """ + running_count = sum(1 for i in instances if i.get("status") == "running") + pending_count = sum(1 for i in instances if i.get("status") in ("pending", "starting")) + failed_count = sum(1 for i in instances if i.get("status") in ("failed", "error")) + target_units = target_capacity if target_capacity is not None else requested_count + + if fleet_type == AWSFleetType.INSTANT: + # Instant fleet: synchronous result, count-based (same as RunInstances) + if running_count >= requested_count and failed_count == 0: + return ProviderFulfilment( + state="fulfilled", + message=f"Instant fleet: {running_count} instance(s) running", + target_units=target_units, + fulfilled_units=running_count, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + elif pending_count > 0: + return ProviderFulfilment( + state="in_progress", + message=f"Instant fleet: {running_count}/{requested_count} running, {pending_count} pending", + target_units=target_units, + fulfilled_units=running_count, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + else: + # fulfillment_final=True for instant — no more instances coming + if running_count > 0: + return ProviderFulfilment( + state="partial", + message=f"Instant fleet: {running_count}/{requested_count} instance(s) running", + target_units=target_units, + fulfilled_units=running_count, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + elif not instances: + return ProviderFulfilment( + state="in_progress", + message="Instant fleet: waiting for instances", + target_units=target_units, + fulfilled_units=0, + running_count=0, + pending_count=0, + failed_count=0, + ) + else: + return ProviderFulfilment( + state="failed", + message="Instant fleet: all instances failed", + target_units=target_units, + fulfilled_units=0, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + else: + # Maintain / Request fleet: capacity-unit based fulfilment + return compute_capacity_based_fulfilment( + target_capacity=target_capacity, + fulfilled_capacity=fulfilled_capacity, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + provider_label="Fleet", + ) + def release_hosts( self, machine_ids: list[str], @@ -729,7 +887,7 @@ def get_example_templates(cls) -> list[Template]: description="EC2 Fleet with instant fulfillment using on-demand instances", provider_api="EC2Fleet", machine_types={"t3.medium": 2, "t3.xlarge": 4}, - max_instances=10, + max_instances=100, price_type="ondemand", subnet_ids=[], security_group_ids=[], @@ -742,7 +900,7 @@ def get_example_templates(cls) -> list[Template]: description="EC2 Fleet with instant fulfillment using spot instances", provider_api="EC2Fleet", machine_types={"t3.medium": 2, "t3.xlarge": 4}, - max_instances=10, + max_instances=100, price_type="spot", max_price=0.10, subnet_ids=[], @@ -756,7 +914,7 @@ def get_example_templates(cls) -> list[Template]: description="EC2 Fleet with instant fulfillment using mixed pricing", provider_api="EC2Fleet", machine_types={"t3.medium": 2, "t3.xlarge": 4}, - max_instances=10, + max_instances=100, price_type="heterogeneous", percent_on_demand=30, allocation_strategy="diversified", @@ -773,7 +931,7 @@ def get_example_templates(cls) -> list[Template]: description="EC2 Fleet with request fulfillment using on-demand instances", provider_api="EC2Fleet", machine_types={"t3.medium": 2, "t3.xlarge": 4}, - max_instances=15, + max_instances=100, price_type="ondemand", subnet_ids=[], security_group_ids=[], @@ -786,7 +944,7 @@ def get_example_templates(cls) -> list[Template]: description="EC2 Fleet with request fulfillment using spot instances", provider_api="EC2Fleet", machine_types={"t3.medium": 2, "t3.xlarge": 4}, - max_instances=20, + max_instances=100, price_type="spot", allocation_strategy="capacityOptimized", max_price=0.10, @@ -801,7 +959,7 @@ def get_example_templates(cls) -> list[Template]: description="EC2 Fleet with request fulfillment using mixed pricing", provider_api="EC2Fleet", machine_types={"t3.medium": 2, "t3.large": 2, "t3.xlarge": 4}, - max_instances=25, + max_instances=100, price_type="heterogeneous", percent_on_demand=40, allocation_strategy="diversified", @@ -819,7 +977,7 @@ def get_example_templates(cls) -> list[Template]: description="EC2 Fleet with maintain capacity using on-demand instances", provider_api="EC2Fleet", machine_types={"t3.medium": 2, "t3.xlarge": 4}, - max_instances=12, + max_instances=100, price_type="ondemand", subnet_ids=[], security_group_ids=[], @@ -832,7 +990,7 @@ def get_example_templates(cls) -> list[Template]: description="EC2 Fleet with maintain capacity using spot instances", provider_api="EC2Fleet", machine_types={"t3.medium": 2, "t3.xlarge": 4}, - max_instances=30, + max_instances=100, price_type="spot", allocation_strategy="priceCapacityOptimized", max_price=0.10, @@ -847,7 +1005,7 @@ def get_example_templates(cls) -> list[Template]: description="EC2 Fleet with maintain capacity using mixed pricing", provider_api="EC2Fleet", machine_types={"t3.medium": 2, "t3.large": 2, "t3.xlarge": 4}, - max_instances=50, + max_instances=100, price_type="heterogeneous", percent_on_demand=50, allocation_strategy="capacityOptimized", diff --git a/src/orb/providers/aws/infrastructure/handlers/ec2_fleet/release_manager.py b/src/orb/providers/aws/infrastructure/handlers/ec2_fleet/release_manager.py index c9e21ae79..0c9d60b18 100644 --- a/src/orb/providers/aws/infrastructure/handlers/ec2_fleet/release_manager.py +++ b/src/orb/providers/aws/infrastructure/handlers/ec2_fleet/release_manager.py @@ -118,20 +118,26 @@ def release( ) if instance_ids: + weighted_capacity_to_return = self._sum_weighted_capacity( + fleet_id, fleet_details, instance_ids + ) + decision = compute_fleet_release_decision( fleet_type=fleet_type, current_capacity=current_capacity, - instances_to_return=len(instance_ids), + weighted_capacity_to_return=weighted_capacity_to_return, ) if decision.requires_capacity_reduction: - new_capacity = max(0, current_capacity - len(instance_ids)) + new_capacity = max(0, current_capacity - weighted_capacity_to_return) self._logger.info( - "Reducing %s fleet %s capacity from %s to %s before terminating instances", + "Reducing %s fleet %s capacity from %s to %s " + "(weighted_capacity_to_return=%s) before terminating instances", fleet_type, fleet_id, current_capacity, new_capacity, + weighted_capacity_to_return, ) self._retry( self._aws_client.ec2_client.modify_fleet, @@ -147,8 +153,43 @@ def release( ) self._logger.info("Terminated EC2 Fleet %s instances: %s", fleet_id, instance_ids) - if decision.is_full_return and decision.has_fleet_record: - self._logger.info("EC2 Fleet %s capacity is zero, deleting fleet", fleet_id) + # Determine whether all fleet instances have been returned. + # decision.is_full_return is based on the weighted capacity sum, so it + # correctly handles weighted fleets. The secondary instance-count check + # below acts as a defensive net for races where the fleet might have been + # partially refilled between our capacity-reduce call and here. + should_delete_fleet = decision.is_full_return + if not should_delete_fleet and decision.has_fleet_record: + # Weighted-capacity fallback: the fleet may be logically empty even + # if the capacity arithmetic suggests otherwise. + should_delete_fleet = self._fleet_has_no_remaining_instances( + fleet_id, set(instance_ids) + ) + if should_delete_fleet: + self._logger.info( + "EC2 Fleet %s has no remaining active instances " + "(weighted-capacity case); treating as full return", + fleet_id, + ) + # Force capacity to 0 before deletion to prevent AWS from + # launching replacement instances during the delete window. + if decision.requires_capacity_reduction: + try: + self._retry( + self._aws_client.ec2_client.modify_fleet, + operation_type="critical", + FleetId=fleet_id, + TargetCapacitySpecification={"TotalTargetCapacity": 0}, + ) + except Exception as exc: + self._logger.warning( + "Failed to zero EC2 Fleet %s capacity before deletion: %s", + fleet_id, + exc, + ) + + if should_delete_fleet and decision.has_fleet_record: + self._logger.info("EC2 Fleet %s is empty, deleting fleet", fleet_id) if decision.requires_capacity_reduction: # maintain fleet — use _delete_fleet (TerminateInstances=True) self._delete_fleet(fleet_id) @@ -162,7 +203,26 @@ def release( ) self._maybe_cleanup_launch_template(fleet_details) elif not decision.has_fleet_record: - # instant fleet — already deleted by AWS; only clean up the launch template. + # instant fleet — AWS may or may not have auto-deleted the fleet record + # (on-demand instant fleets in particular can remain in "active" state + # indefinitely). Always attempt an explicit delete so the fleet is + # removed promptly; swallow any error in case it was already gone. + try: + self._retry( + self._aws_client.ec2_client.delete_fleets, + operation_type="critical", + FleetIds=[fleet_id], + TerminateInstances=True, + ) + self._logger.info( + "Deleted instant EC2 Fleet %s (instances already terminated)", fleet_id + ) + except Exception as exc: + self._logger.warning( + "Could not delete instant EC2 Fleet %s (may already be gone): %s", + fleet_id, + exc, + ) self._maybe_cleanup_launch_template(fleet_details) else: self._delete_fleet(fleet_id) @@ -230,6 +290,122 @@ def find_fleet_for_instance(self, instance_id: str) -> Optional[str]: # Private helpers # ------------------------------------------------------------------ + def _fleet_has_no_remaining_instances(self, fleet_id: str, excluded_ids: set[str]) -> bool: + """Return True when the EC2 Fleet has no active instances outside *excluded_ids*. + + Used as a secondary full-return detector for weighted fleets where the + capacity arithmetic alone is insufficient: a single instance with + WeightedCapacity > 1 can satisfy a TotalTargetCapacity > 1, but the + capacity counter only decrements by 1 per instance detached. + + Args: + fleet_id: EC2 Fleet ID to inspect. + excluded_ids: Instance IDs that have already been submitted for + termination and should be treated as gone. + + Returns: + True when no active instances remain, False when any do (or on error). + """ + try: + active = self._collect_with_next_token( + self._aws_client.ec2_client.describe_fleet_instances, + "ActiveInstances", + FleetId=fleet_id, + ) + remaining = [inst for inst in active if inst.get("InstanceId") not in excluded_ids] + return len(remaining) == 0 + except Exception as exc: + self._logger.warning( + "Could not verify remaining instances for EC2 Fleet %s: %s — " + "assuming non-empty (safe default)", + fleet_id, + exc, + ) + # Safe default: assume the fleet still has instances rather than + # accidentally deleting a fleet that has active instances. + return False + + def _sum_weighted_capacity( + self, + fleet_id: str, + fleet_details: dict[str, Any], + instance_ids: list[str], + ) -> int: + """Return the total WeightedCapacity consumed by *instance_ids* in this fleet. + + Queries ``describe_fleet_instances`` to find the InstanceType for each + active instance, then looks up the WeightedCapacity for that type in + ``LaunchTemplateConfigs[].Overrides[].WeightedCapacity``. + + Instances that are not present in ``ActiveInstances`` (already terminated + or in the middle of a race) default to a weight of 1 so that the capacity + arithmetic errs on the side of *too small* a decrement rather than leaving + orphaned capacity that AWS would refill. + + Args: + fleet_id: EC2 Fleet ID. + fleet_details: Pre-fetched DescribeFleets entry for this fleet. + instance_ids: The specific instance IDs being returned. + + Returns: + Sum of weighted capacity units to subtract from TotalTargetCapacity. + """ + # Build a map of instance_type → WeightedCapacity from the fleet launch spec. + weight_by_type: dict[str, int] = {} + for lt_config in fleet_details.get("LaunchTemplateConfigs", []): + for override in lt_config.get("Overrides", []): + itype = override.get("InstanceType") + raw_weight = override.get("WeightedCapacity") + if itype and raw_weight is not None: + try: + weight_by_type[itype] = int(raw_weight) + except (TypeError, ValueError): + pass + + if not weight_by_type: + # Fleet has no WeightedCapacity overrides at all — each instance counts as 1. + # Skip the describe_fleet_instances API call entirely; result is just the count. + self._logger.debug( + "EC2 Fleet %s has no WeightedCapacity overrides; " + "using instance count %d as capacity decrement", + fleet_id, + len(instance_ids), + ) + return max(1, len(instance_ids)) + + # Fetch the current active instances so we know each instance's type. + instance_type_by_id: dict[str, str] = {} + try: + active = self._collect_with_next_token( + self._aws_client.ec2_client.describe_fleet_instances, + "ActiveInstances", + FleetId=fleet_id, + ) + for item in active: + iid = item.get("InstanceId") + itype = item.get("InstanceType") + if iid and itype: + instance_type_by_id[iid] = itype + except Exception as exc: + self._logger.warning( + "Could not fetch active instances for EC2 Fleet %s to compute " + "weighted capacity; defaulting all instance weights to 1: %s", + fleet_id, + exc, + ) + + total = 0 + for iid in instance_ids: + itype = instance_type_by_id.get(iid) + if itype and itype in weight_by_type: + total += weight_by_type[itype] + else: + # Instance not found in ActiveInstances (already terminated / race), + # or instance type has no explicit weight → default to 1. + total += 1 + + return max(1, total) + def _delete_fleet(self, fleet_id: str) -> None: """Delete an EC2 Fleet, terminating its instances.""" try: diff --git a/src/orb/providers/aws/infrastructure/handlers/fleet_release_policy.py b/src/orb/providers/aws/infrastructure/handlers/fleet_release_policy.py index 12fd147e1..9d8718294 100644 --- a/src/orb/providers/aws/infrastructure/handlers/fleet_release_policy.py +++ b/src/orb/providers/aws/infrastructure/handlers/fleet_release_policy.py @@ -17,7 +17,7 @@ class FleetReleaseDecision: def compute_fleet_release_decision( fleet_type: str, current_capacity: int, - instances_to_return: int, + weighted_capacity_to_return: int, ) -> FleetReleaseDecision: """Compute what actions are required when returning instances from a fleet. @@ -25,7 +25,10 @@ def compute_fleet_release_decision( fleet_type: Raw fleet type string (e.g. "maintain", "request", "instant"). Accepts enum values — normalised to lowercase string internally. current_capacity: Current TotalTargetCapacity / TargetCapacity of the fleet. - instances_to_return: Number of instances being returned in this call. + weighted_capacity_to_return: Sum of WeightedCapacity for all instances being + returned. For unweighted fleets this equals the instance count. + Using the weighted sum prevents a race window where AWS refills + capacity units that were not decremented by the right amount. Returns: A FleetReleaseDecision describing which actions the caller must take. @@ -38,7 +41,7 @@ def compute_fleet_release_decision( raw = fleet_type.value # type: ignore[union-attr] fleet_type_lower = str(raw).lower() - remaining = max(0, current_capacity - instances_to_return) + remaining = max(0, current_capacity - weighted_capacity_to_return) is_full = remaining == 0 if fleet_type_lower == "maintain": diff --git a/src/orb/providers/aws/infrastructure/handlers/run_instances/handler.py b/src/orb/providers/aws/infrastructure/handlers/run_instances/handler.py index 14ae00f01..578005a18 100644 --- a/src/orb/providers/aws/infrastructure/handlers/run_instances/handler.py +++ b/src/orb/providers/aws/infrastructure/handlers/run_instances/handler.py @@ -34,6 +34,7 @@ from orb.domain.base.dependency_injection import injectable from orb.domain.base.ports import ErrorHandlingPort, LoggingPort from orb.domain.base.ports.configuration_port import ConfigurationPort +from orb.domain.base.provider_fulfilment import CheckHostsStatusResult, ProviderFulfilment from orb.domain.request.aggregate import Request from orb.domain.template.template_aggregate import Template from orb.infrastructure.adapters.ports.request_adapter_port import RequestAdapterPort @@ -410,8 +411,15 @@ def _create_run_instances_params_legacy( return params - def check_hosts_status(self, request: Request) -> list[dict[str, Any]]: - """Check the status of instances created by RunInstances.""" + def check_hosts_status(self, request: Request) -> CheckHostsStatusResult: + """Check the status of instances created by RunInstances. + + Fulfilment semantics: + RunInstances is a synchronous API — each instance equals 1 capacity + unit. The request is ``fulfilled`` when running_count >= requested_count + and failed_count == 0. If the describe response is empty we treat + that as still in_progress so the next poll can confirm. + """ try: # Get instance IDs from provider_data (preferred) or fallback to metadata for backward compatibility instance_ids = ( @@ -433,7 +441,17 @@ def check_hosts_status(self, request: Request) -> list[dict[str, Any]]: "No instance IDs or resource IDs found in request %s", request.request_id, ) - return [] + return CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message="No instance IDs yet — waiting for provisioning", + target_units=request.requested_count, + running_count=0, + pending_count=0, + failed_count=0, + ), + ) # Get resource ID from provider_data (preferred) or domain field or fallback to metadata resource_id = ( @@ -459,17 +477,85 @@ def check_hosts_status(self, request: Request) -> list[dict[str, Any]]: provider_api="RunInstances", ) - return self._format_instance_data( + instances = self._format_instance_data( instance_details, resource_id, self._resolve_provider_api(request) ) + fulfilment = self._compute_run_instances_fulfilment(instances, request.requested_count) + return CheckHostsStatusResult(instances=instances, fulfilment=fulfilment) except Exception as e: self._logger.error("Unexpected error checking RunInstances status: %s", str(e)) raise AWSInfrastructureError(f"Failed to check RunInstances status: {e!s}") + def _compute_run_instances_fulfilment( + self, instances: list[dict[str, Any]], requested_count: int + ) -> ProviderFulfilment: + """Compute ProviderFulfilment for a RunInstances-style request. + + RunInstances is sync/simple: 1 instance == 1 capacity unit. + The request is fulfilled when running_count >= requested_count and + no failed instances exist. + """ + running_count = sum(1 for i in instances if i.get("status") == "running") + pending_count = sum(1 for i in instances if i.get("status") in ("pending", "starting")) + failed_count = sum( + 1 for i in instances if i.get("status") in ("failed", "error", "shutting-down") + ) + + if running_count >= requested_count and failed_count == 0: + return ProviderFulfilment( + state="fulfilled", + message=f"All {running_count} instance(s) running", + target_units=requested_count, + fulfilled_units=running_count, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + elif pending_count > 0: + return ProviderFulfilment( + state="in_progress", + message=f"{running_count}/{requested_count} running, {pending_count} pending", + target_units=requested_count, + fulfilled_units=running_count, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + elif failed_count == len(instances) and len(instances) > 0: + return ProviderFulfilment( + state="failed", + message=f"All {failed_count} instance(s) failed", + target_units=requested_count, + fulfilled_units=0, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + elif running_count > 0: + return ProviderFulfilment( + state="partial", + message=f"{running_count}/{requested_count} instance(s) running", + target_units=requested_count, + fulfilled_units=running_count, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + else: + return ProviderFulfilment( + state="in_progress", + message="Instances starting", + target_units=requested_count, + fulfilled_units=0, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + def _find_instances_by_resource_ids( self, request: Request, resource_ids: list[str] - ) -> list[dict[str, Any]]: + ) -> CheckHostsStatusResult: """Find instances using resource IDs (reservation IDs for RunInstances).""" try: all_instances: list[dict[str, Any]] = [] @@ -522,7 +608,10 @@ def _find_instances_by_resource_ids( resource_ids, ) - return all_instances + fulfilment = self._compute_run_instances_fulfilment( + all_instances, request.requested_count + ) + return CheckHostsStatusResult(instances=all_instances, fulfilment=fulfilment) except Exception as e: self._logger.error( @@ -534,7 +623,7 @@ def _find_instances_by_resource_ids( def _find_instances_by_tags_fallback( self, request: Request, resource_ids: list[str] - ) -> list[dict[str, Any]]: + ) -> CheckHostsStatusResult: """Fallback method to find instances by tags when reservation-id filter is not supported.""" try: response = self.aws_client.ec2_client.describe_instances() @@ -565,11 +654,24 @@ def _find_instances_by_tags_fallback( ) ) - return formatted_instances + fulfilment = self._compute_run_instances_fulfilment( + formatted_instances, request.requested_count + ) + return CheckHostsStatusResult(instances=formatted_instances, fulfilment=fulfilment) except Exception as e: self._logger.error("FALLBACK: Fallback method failed to find instances: %s", e) - return [] + return CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message="Instance lookup failed — will retry", + target_units=request.requested_count, + running_count=0, + pending_count=0, + failed_count=0, + ), + ) def release_hosts( self, @@ -677,7 +779,7 @@ def get_example_templates(cls) -> list[Template]: description="On-demand instances using RunInstances API", provider_api="RunInstances", machine_types={"t3.medium": 2}, - max_instances=5, + max_instances=100, price_type="ondemand", subnet_ids=[], security_group_ids=[], @@ -689,7 +791,7 @@ def get_example_templates(cls) -> list[Template]: description="Spot instances using RunInstances API", provider_api="RunInstances", machine_types={"t3.medium": 2}, - max_instances=10, + max_instances=100, price_type="spot", max_price=0.10, subnet_ids=[], diff --git a/src/orb/providers/aws/infrastructure/handlers/shared/fleet_fulfilment.py b/src/orb/providers/aws/infrastructure/handlers/shared/fleet_fulfilment.py new file mode 100644 index 000000000..15bdad127 --- /dev/null +++ b/src/orb/providers/aws/infrastructure/handlers/shared/fleet_fulfilment.py @@ -0,0 +1,74 @@ +"""Shared capacity-based fleet fulfilment computation. + +Both EC2 Fleet (Maintain/Request types) and Spot Fleet share identical +fulfilment semantics: FulfilledCapacity >= TargetCapacity AND no pending +or failed instances → fulfilled. The only difference is the label used +in human-readable messages. +""" + +from __future__ import annotations + +from typing import Optional + +from orb.domain.base.provider_fulfilment import ProviderFulfilment + + +def compute_capacity_based_fulfilment( + target_capacity: Optional[int], + fulfilled_capacity: float, + running_count: int, + pending_count: int, + failed_count: int, + provider_label: str, +) -> ProviderFulfilment: + """Compute ProviderFulfilment for a capacity-unit based fleet. + + Used by EC2 Fleet (Maintain/Request) and Spot Fleet handlers. + + Args: + target_capacity: The fleet's TargetCapacity, or None if unknown. + fulfilled_capacity: The fleet's FulfilledCapacity as reported by AWS. + running_count: Number of instances whose status is "running". + pending_count: Number of instances whose status is "pending" or "starting". + failed_count: Number of instances whose status is "failed" or "error". + provider_label: Label used in messages, e.g. "Fleet" or "Spot Fleet". + """ + target_units = target_capacity if target_capacity is not None else int(fulfilled_capacity) + fleet_fully_fulfilled = target_capacity is not None and fulfilled_capacity >= target_capacity + + if fleet_fully_fulfilled and pending_count == 0 and failed_count == 0: + return ProviderFulfilment( + state="fulfilled", + message=( + f"{provider_label} fulfilled: {running_count} instance(s) running " + f"({fulfilled_capacity}/{target_capacity} capacity units)" + ), + target_units=target_units, + fulfilled_units=int(fulfilled_capacity), + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + elif failed_count > 0 and running_count == 0 and pending_count == 0: + return ProviderFulfilment( + state="failed", + message=f"{provider_label} failed: {failed_count} instance(s) failed", + target_units=target_units, + fulfilled_units=int(fulfilled_capacity), + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) + else: + return ProviderFulfilment( + state="in_progress", + message=( + f"{provider_label}: {running_count} running, {pending_count} pending " + f"({fulfilled_capacity}/{target_units} capacity units)" + ), + target_units=target_units, + fulfilled_units=int(fulfilled_capacity), + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + ) diff --git a/src/orb/providers/aws/infrastructure/handlers/shared/fleet_grouping_mixin.py b/src/orb/providers/aws/infrastructure/handlers/shared/fleet_grouping_mixin.py index 804ec7285..11b937459 100644 --- a/src/orb/providers/aws/infrastructure/handlers/shared/fleet_grouping_mixin.py +++ b/src/orb/providers/aws/infrastructure/handlers/shared/fleet_grouping_mixin.py @@ -119,11 +119,14 @@ def _group_instances_from_mapping( self._collect_groups_from_instances( instances_needing_lookup, groups, group_ids_to_fetch ) - # Fetch details only for groups discovered via AWS lookup — groups resolved - # from the mapping are left with details=None so the release manager fetches - # them on demand (avoiding unnecessary describe calls on the return path). - if group_ids_to_fetch: - self._fetch_and_attach_group_details(group_ids_to_fetch, groups) + + # Fetch details for all known group IDs regardless of how they were resolved. + # When all instances come from resource_mapping, instances_needing_lookup is empty + # but group_ids_to_fetch is still populated from the mapping loop above. Without + # this fetch, group details remain None and release_manager falls back to a raw + # describe that reads MinSize directly from AWS, creating a double-decrement risk. + if group_ids_to_fetch: + self._fetch_and_attach_group_details(group_ids_to_fetch, groups) # Log performance metrics and optimization results self._log_grouping_summary( diff --git a/src/orb/providers/aws/infrastructure/handlers/spot_fleet/config_builder.py b/src/orb/providers/aws/infrastructure/handlers/spot_fleet/config_builder.py index f8ebc4910..7dd083ae5 100644 --- a/src/orb/providers/aws/infrastructure/handlers/spot_fleet/config_builder.py +++ b/src/orb/providers/aws/infrastructure/handlers/spot_fleet/config_builder.py @@ -268,7 +268,7 @@ def _build_legacy( if price_type in ("ondemand", "heterogeneous") or on_demand_capacity > 0: fleet_config["OnDemandTargetCapacity"] = on_demand_capacity - if template.fleet_type == AWSFleetType.MAINTAIN.value: + if template.fleet_type == AWSFleetType.MAINTAIN: fleet_config["ReplaceUnhealthyInstances"] = True fleet_config["TerminateInstancesWithExpiration"] = True diff --git a/src/orb/providers/aws/infrastructure/handlers/spot_fleet/handler.py b/src/orb/providers/aws/infrastructure/handlers/spot_fleet/handler.py index 35565fae7..b58601f5b 100644 --- a/src/orb/providers/aws/infrastructure/handlers/spot_fleet/handler.py +++ b/src/orb/providers/aws/infrastructure/handlers/spot_fleet/handler.py @@ -31,6 +31,7 @@ from orb.domain.base.dependency_injection import injectable from orb.domain.base.ports import LoggingPort from orb.domain.base.ports.configuration_port import ConfigurationPort +from orb.domain.base.provider_fulfilment import CheckHostsStatusResult, ProviderFulfilment from orb.domain.request.aggregate import Request from orb.domain.template.template_aggregate import Template from orb.infrastructure.adapters.ports.request_adapter_port import RequestAdapterPort @@ -44,6 +45,9 @@ from orb.providers.aws.infrastructure.aws_client import AWSClient from orb.providers.aws.infrastructure.handlers.base_handler import AWSHandler from orb.providers.aws.infrastructure.handlers.shared.base_context_mixin import BaseContextMixin +from orb.providers.aws.infrastructure.handlers.shared.fleet_fulfilment import ( + compute_capacity_based_fulfilment, +) from orb.providers.aws.infrastructure.handlers.shared.fleet_grouping_mixin import FleetGroupingMixin from orb.providers.aws.infrastructure.handlers.spot_fleet.config_builder import ( SpotFleetConfigBuilder, @@ -241,44 +245,91 @@ def _resolve_fleet_role(self, aws_template: AWSTemplate) -> AWSTemplate: aws_template = aws_template.model_copy(update={"fleet_role": resolved}) return aws_template - def check_hosts_status(self, request: Request) -> list[dict[str, Any]]: - """Check the status of instances across all spot fleets in the request.""" + def check_hosts_status(self, request: Request) -> CheckHostsStatusResult: + """Check the status of instances across all spot fleets in the request. + + Fulfilment semantics (same as EC2Fleet Maintain/Request): + FulfilledCapacity >= TargetCapacity AND pending_count == 0 + AND failed_count == 0 → fulfilled. + """ try: if not request.resource_ids: self._logger.info("No Spot Fleet Request IDs found in request") - return [] + return CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message="No Spot Fleet IDs yet — waiting for provisioning", + target_units=request.requested_count, + running_count=0, + pending_count=0, + failed_count=0, + ), + ) - all_instances = [] + all_instances: list[dict[str, Any]] = [] + fleet_results: list[CheckHostsStatusResult] = [] request_id = str(request.request_id) - # Process all fleet IDs instead of just the first one for fleet_id in request.resource_ids: try: - fleet_instances = self._get_spot_fleet_instances( - fleet_id, request_id=request_id + result = self._get_spot_fleet_status( + fleet_id, + request_id=request_id, + requested_count=request.requested_count, ) - if fleet_instances: - formatted_instances = self._format_instance_data( - fleet_instances, fleet_id, self._resolve_provider_api(request) - ) - all_instances.extend(formatted_instances) + all_instances.extend(result.instances) + fleet_results.append(result) except Exception as e: self._logger.error("Failed to get instances for spot fleet %s: %s", fleet_id, e) continue - return all_instances + if not fleet_results: + return CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message="No Spot Fleet status available — will retry", + ), + ) + + if len(fleet_results) == 1: + return CheckHostsStatusResult( + instances=all_instances, + fulfilment=fleet_results[0].fulfilment, + ) + + states = [r.fulfilment.state for r in fleet_results] + if all(s == "fulfilled" for s in states): + combined_state = "fulfilled" + combined_msg = f"All {len(fleet_results)} spot fleets fulfilled" + elif any(s == "failed" for s in states): + combined_state = "failed" + combined_msg = "One or more spot fleets failed" + elif any(s == "partial" for s in states): + combined_state = "partial" + combined_msg = "One or more spot fleets partially fulfilled" + else: + combined_state = "in_progress" + combined_msg = "Waiting for spot fleet(s) to fulfil" + + return CheckHostsStatusResult( + instances=all_instances, + fulfilment=ProviderFulfilment(state=combined_state, message=combined_msg), + ) except Exception as e: self._logger.error("Unexpected error checking Spot Fleet status: %s", str(e)) raise AWSInfrastructureError(f"Failed to check Spot Fleet status: {e!s}") - def _get_spot_fleet_instances( + def _get_spot_fleet_status( self, fleet_id: str, - request_id: str = None, # type: ignore[assignment] - ) -> list[dict[str, Any]]: - """Get instances for a specific spot fleet.""" - # Get fleet information + request_id: str = "", + requested_count: int = 1, + ) -> CheckHostsStatusResult: + """Get status + fulfilment for a specific Spot Fleet request.""" + # Get fleet config (includes TargetCapacity and FulfilledCapacity) fleet_list = self._retry_with_backoff( lambda: self._paginate( self.aws_client.ec2_client.describe_spot_fleet_requests, @@ -289,7 +340,23 @@ def _get_spot_fleet_instances( if not fleet_list: self._logger.warning("Spot Fleet Request %s not found", fleet_id) - return [] + return CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message=f"Spot Fleet {fleet_id} not yet visible — waiting", + target_units=requested_count, + running_count=0, + pending_count=0, + failed_count=0, + ), + ) + + fleet_config_entry = fleet_list[0] + fleet_cfg = fleet_config_entry.get("SpotFleetRequestConfig") or {} + target_capacity: Optional[int] = fleet_cfg.get("TargetCapacity") + fulfilled_capacity: float = fleet_cfg.get("FulfilledCapacity") or 0.0 + target_units = target_capacity if target_capacity is not None else requested_count # Get active instances active_instances = self._retry_with_backoff( @@ -301,13 +368,84 @@ def _get_spot_fleet_instances( ) if not active_instances: - return [] + # Fleet submitted but no instances yet — check if capacity has been allocated + fleet_fully_fulfilled = ( + target_capacity is not None and fulfilled_capacity >= target_capacity + ) + if fleet_fully_fulfilled: + # Capacity allocated but instances not visible yet + return CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message=f"Spot Fleet capacity allocated ({fulfilled_capacity}/{target_capacity}), instances starting", + target_units=target_units, + fulfilled_units=int(fulfilled_capacity), + running_count=0, + pending_count=0, + failed_count=0, + ), + ) + return CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message=f"Spot Fleet waiting for instances ({fulfilled_capacity}/{target_units} capacity units)", + target_units=target_units, + fulfilled_units=int(fulfilled_capacity), + running_count=0, + pending_count=0, + failed_count=0, + ), + ) instance_ids = [instance["InstanceId"] for instance in active_instances] - return self._get_instance_details( + instance_details = self._get_instance_details( instance_ids, request_id=request_id, resource_id=fleet_id, provider_api="SpotFleet" ) + fulfilment = self._compute_spot_fleet_fulfilment( + instances=instance_details, + target_capacity=target_capacity, + fulfilled_capacity=fulfilled_capacity, + requested_count=requested_count, + ) + return CheckHostsStatusResult(instances=instance_details, fulfilment=fulfilment) + + def _compute_spot_fleet_fulfilment( + self, + instances: list[dict[str, Any]], + target_capacity: Optional[int], + fulfilled_capacity: float, + requested_count: int, + ) -> ProviderFulfilment: + """Compute ProviderFulfilment for a Spot Fleet request. + + Same semantics as EC2Fleet Maintain/Request: + FulfilledCapacity >= TargetCapacity AND pending_count == 0 + AND failed_count == 0 → fulfilled. + """ + running_count = sum(1 for i in instances if i.get("status") == "running") + pending_count = sum(1 for i in instances if i.get("status") in ("pending", "starting")) + failed_count = sum(1 for i in instances if i.get("status") in ("failed", "error")) + return compute_capacity_based_fulfilment( + target_capacity=target_capacity, + fulfilled_capacity=fulfilled_capacity, + running_count=running_count, + pending_count=pending_count, + failed_count=failed_count, + provider_label="Spot Fleet", + ) + + def _get_spot_fleet_instances( + self, + fleet_id: str, + request_id: str = "", # type: ignore[assignment] + ) -> list[dict[str, Any]]: + """Get raw instance list for a specific spot fleet (legacy helper — use _get_spot_fleet_status for new code).""" + result = self._get_spot_fleet_status(fleet_id, request_id=request_id) + return result.instances + def _resolve_provider_api( self, request: Request, aws_template: Optional[AWSTemplate] = None ) -> str: @@ -535,7 +673,7 @@ def get_example_templates(cls) -> list[Template]: description="Spot Fleet request with lowest price allocation", provider_api="SpotFleet", machine_types={"t3.medium": 2, "t3.large": 2, "t3.xlarge": 4}, - max_instances=20, + max_instances=100, price_type="spot", allocation_strategy="lowestPrice", fleet_type="request", @@ -550,7 +688,7 @@ def get_example_templates(cls) -> list[Template]: description="Spot Fleet request with diversified allocation", provider_api="SpotFleet", machine_types={"t3.medium": 2, "t3.large": 2, "t3.xlarge": 4}, - max_instances=25, + max_instances=100, price_type="spot", allocation_strategy="diversified", fleet_type="request", @@ -565,7 +703,7 @@ def get_example_templates(cls) -> list[Template]: description="Spot Fleet request with capacity optimized allocation", provider_api="SpotFleet", machine_types={"t3.medium": 2, "t3.large": 2, "t3.xlarge": 4}, - max_instances=30, + max_instances=100, price_type="spot", allocation_strategy="capacityOptimized", fleet_type="request", @@ -581,7 +719,7 @@ def get_example_templates(cls) -> list[Template]: description="Spot Fleet maintain with lowest price allocation", provider_api="SpotFleet", machine_types={"t3.medium": 2, "t3.large": 2, "t3.xlarge": 4}, - max_instances=15, + max_instances=100, price_type="spot", allocation_strategy="lowestPrice", fleet_type="maintain", @@ -596,7 +734,7 @@ def get_example_templates(cls) -> list[Template]: description="Spot Fleet maintain with diversified allocation", provider_api="SpotFleet", machine_types={"t3.medium": 2, "t3.large": 2, "t3.xlarge": 4}, - max_instances=20, + max_instances=100, price_type="spot", allocation_strategy="diversified", fleet_type="maintain", @@ -611,7 +749,7 @@ def get_example_templates(cls) -> list[Template]: description="Spot Fleet maintain with capacity optimized allocation", provider_api="SpotFleet", machine_types={"t3.medium": 2, "t3.large": 2, "t3.xlarge": 4}, - max_instances=25, + max_instances=100, price_type="spot", allocation_strategy="capacityOptimized", fleet_type="maintain", diff --git a/src/orb/providers/aws/infrastructure/handlers/spot_fleet/release_manager.py b/src/orb/providers/aws/infrastructure/handlers/spot_fleet/release_manager.py index 2c9d5271b..a601fdb04 100644 --- a/src/orb/providers/aws/infrastructure/handlers/spot_fleet/release_manager.py +++ b/src/orb/providers/aws/infrastructure/handlers/spot_fleet/release_manager.py @@ -71,22 +71,28 @@ def release( on_demand_capacity = int(fleet_config.get("OnDemandTargetCapacity", 0) or 0) if instance_ids: + weighted_capacity_to_return = self._sum_weighted_capacity( + fleet_id, fleet_config, instance_ids + ) + decision = compute_fleet_release_decision( fleet_type=fleet_type, current_capacity=target_capacity, - instances_to_return=len(instance_ids), + weighted_capacity_to_return=weighted_capacity_to_return, ) if decision.requires_capacity_reduction: - new_target_capacity = max(0, target_capacity - len(instance_ids)) + new_target_capacity = max(0, target_capacity - weighted_capacity_to_return) new_on_demand_capacity = min(on_demand_capacity, new_target_capacity) self._logger.info( - "Reducing %s Spot Fleet %s capacity from %s to %s before terminating instances", + "Reducing %s Spot Fleet %s capacity from %s to %s " + "(weighted_capacity_to_return=%s) before terminating instances", fleet_type, fleet_id, target_capacity, new_target_capacity, + weighted_capacity_to_return, ) self._retry( @@ -102,8 +108,40 @@ def release( ) self._logger.info("Terminated Spot Fleet %s instances: %s", fleet_id, instance_ids) - if decision.is_full_return and decision.has_fleet_record: - self._logger.info("Spot Fleet %s capacity is zero, cancelling fleet", fleet_id) + # Determine whether all fleet instances have been returned. + # decision.is_full_return is based on the weighted capacity sum, so it + # correctly handles weighted fleets. The secondary instance-count check + # below acts as a defensive net for races. + should_cancel_fleet = decision.is_full_return + if not should_cancel_fleet and decision.has_fleet_record: + should_cancel_fleet = self._fleet_has_no_remaining_instances( + fleet_id, set(instance_ids) + ) + if should_cancel_fleet: + self._logger.info( + "Spot Fleet %s has no remaining active instances " + "(weighted-capacity case); treating as full return", + fleet_id, + ) + # Zero the capacity to prevent replacement before cancellation. + if decision.requires_capacity_reduction: + try: + self._retry( + self._aws_client.ec2_client.modify_spot_fleet_request, + operation_type="critical", + SpotFleetRequestId=fleet_id, + TargetCapacity=0, + OnDemandTargetCapacity=0, + ) + except Exception as exc: + self._logger.warning( + "Failed to zero Spot Fleet %s capacity before cancellation: %s", + fleet_id, + exc, + ) + + if should_cancel_fleet and decision.has_fleet_record: + self._logger.info("Spot Fleet %s is empty, cancelling fleet", fleet_id) self._retry( self._aws_client.ec2_client.cancel_spot_fleet_requests, operation_type="critical", @@ -177,6 +215,134 @@ def find_fleet_for_instance(self, instance_id: str) -> Optional[str]: # Private helpers # ------------------------------------------------------------------ + def _fleet_has_no_remaining_instances(self, fleet_id: str, excluded_ids: set[str]) -> bool: + """Return True when the Spot Fleet has no active instances outside *excluded_ids*. + + Used as a secondary full-return detector for weighted fleets where the + capacity arithmetic alone is insufficient. + + Args: + fleet_id: Spot Fleet request ID to inspect. + excluded_ids: Instance IDs that have already been submitted for + termination and should be treated as gone. + + Returns: + True when no active instances remain, False when any do (or on error). + """ + try: + resp = self._retry( + self._aws_client.ec2_client.describe_spot_fleet_instances, + operation_type="read_only", + SpotFleetRequestId=fleet_id, + ) + active = resp.get("ActiveInstances", []) + remaining = [inst for inst in active if inst.get("InstanceId") not in excluded_ids] + return len(remaining) == 0 + except Exception as exc: + self._logger.warning( + "Could not verify remaining instances for Spot Fleet %s: %s — " + "assuming non-empty (safe default)", + fleet_id, + exc, + ) + return False + + def _sum_weighted_capacity( + self, + fleet_id: str, + fleet_config: dict[str, Any], + instance_ids: list[str], + ) -> int: + """Return the total WeightedCapacity consumed by *instance_ids* in this Spot Fleet. + + Queries ``describe_spot_fleet_instances`` (which includes ``WeightedCapacity`` + directly on each ``ActiveInstances`` entry) to resolve each returning instance's + weight. Falls back to the ``LaunchSpecifications`` / ``LaunchTemplateConfigs`` + weight-by-type map when an instance is absent from ``ActiveInstances`` + (already terminated or in a race). Instances with no resolvable weight + default to 1. + + Args: + fleet_id: Spot Fleet request ID. + fleet_config: ``SpotFleetRequestConfig`` dict from the describe response. + instance_ids: The specific instance IDs being returned. + + Returns: + Sum of weighted capacity units to subtract from TargetCapacity. + """ + # Build a fallback map of instance_type → WeightedCapacity from the fleet spec. + weight_by_type: dict[str, int] = {} + for spec in fleet_config.get("LaunchSpecifications", []): + itype = spec.get("InstanceType") + raw_weight = spec.get("WeightedCapacity") + if itype and raw_weight is not None: + try: + weight_by_type[itype] = int(float(raw_weight)) + except (TypeError, ValueError): + pass + for lt_config in fleet_config.get("LaunchTemplateConfigs", []): + for override in lt_config.get("Overrides", []): + itype = override.get("InstanceType") + raw_weight = override.get("WeightedCapacity") + if itype and raw_weight is not None: + try: + weight_by_type[itype] = int(float(raw_weight)) + except (TypeError, ValueError): + pass + + # Fetch the active instance list; the API returns WeightedCapacity per entry. + weight_by_instance_id: dict[str, int] = {} + instance_type_by_id: dict[str, str] = {} + try: + resp = self._retry( + self._aws_client.ec2_client.describe_spot_fleet_instances, + operation_type="read_only", + SpotFleetRequestId=fleet_id, + ) + for item in resp.get("ActiveInstances", []): + iid = item.get("InstanceId") + itype = item.get("InstanceType") + raw_weight = item.get("WeightedCapacity") + if iid: + if itype: + instance_type_by_id[iid] = itype + if raw_weight is not None: + try: + weight_by_instance_id[iid] = int(float(raw_weight)) + except (TypeError, ValueError): + pass + except Exception as exc: + self._logger.warning( + "Could not fetch active instances for Spot Fleet %s to compute " + "weighted capacity; defaulting all instance weights to 1: %s", + fleet_id, + exc, + ) + + total = 0 + for iid in instance_ids: + # Prefer the per-instance weight from the live describe response. + if iid in weight_by_instance_id: + total += weight_by_instance_id[iid] + else: + # Fall back to the weight-by-type map from the fleet config. + itype = instance_type_by_id.get(iid) + if itype and itype in weight_by_type: + total += weight_by_type[itype] + else: + # Instance not found or type has no weight → default to 1. + total += 1 + + if not weight_by_type: + self._logger.debug( + "Spot Fleet %s has no WeightedCapacity overrides; " + "using instance count %d as capacity decrement", + fleet_id, + len(instance_ids), + ) + + return max(1, total) + def _retry(self, func: Any, operation_type: str = "standard", **kwargs: Any) -> Any: """Delegate to the injected retry function if available, else call directly.""" if self._retry_fn is not None: diff --git a/src/orb/providers/aws/registration.py b/src/orb/providers/aws/registration.py index f33060361..f64fb05c6 100644 --- a/src/orb/providers/aws/registration.py +++ b/src/orb/providers/aws/registration.py @@ -1,6 +1,8 @@ """AWS Provider Registration - Register AWS provider with the provider registry.""" +import json from contextlib import suppress +from pathlib import Path from typing import TYPE_CHECKING, Any, Optional # Use TYPE_CHECKING to avoid direct infrastructure import @@ -9,9 +11,10 @@ from orb.providers.registry import ProviderRegistry # Template extension imports for our new functionality -from orb.domain.template.extensions import TemplateExtensionRegistry from orb.domain.template.factory import TemplateFactory +from orb.infrastructure.registry.template_extension_registry import TemplateExtensionRegistry from orb.providers.aws.configuration.template_extension import AWSTemplateExtensionConfig +from orb.providers.aws.domain.template.aws_template_dto_config import AWSTemplateDTOConfig def create_aws_strategy(provider_config: Any) -> Any: @@ -93,7 +96,11 @@ def create_aws_strategy(provider_config: Any) -> Any: if strategy.aws_client is not None: health_check = get_container().get(HealthCheckPort) - register_aws_health_checks(health_check, strategy.aws_client) + _storage_strategy = "json" + if config_port is not None: + with suppress(Exception): + _storage_strategy = config_port.get_storage_strategy() + register_aws_health_checks(health_check, strategy.aws_client, _storage_strategy) # Set provider name for identification if hasattr(strategy, "name") and provider_name: @@ -197,6 +204,26 @@ def create_aws_validator(provider_config: Any = None) -> Any: raise RuntimeError(f"Failed to create AWS validator: {e!s}") +def _load_aws_default_api() -> Optional[str]: + """Extract the default provider API from aws_defaults.json. + + Returns the value at provider.provider_defaults.aws.template_defaults.provider_api, + or None if the file cannot be read or the key is absent. + """ + try: + defaults_file = Path(__file__).parent / "config" / "aws_defaults.json" + data = json.loads(defaults_file.read_text()) + return ( + data.get("provider", {}) + .get("provider_defaults", {}) + .get("aws", {}) + .get("template_defaults", {}) + .get("provider_api") + ) + except Exception: + return None + + def register_aws_provider( registry: "Optional[ProviderRegistry]" = None, logger: "Optional[LoggingPort]" = None, @@ -237,6 +264,7 @@ def register_aws_provider( resolver_factory=create_aws_resolver, validator_factory=create_aws_validator, strategy_class=AWSProviderStrategy, + default_api=_load_aws_default_api(), ) # Register AWS template store @@ -358,12 +386,13 @@ def register_aws_extensions(logger: Optional["LoggingPort"] = None) -> None: logger: Optional logger for registration messages """ try: - # Register AWS template extension configuration - TemplateExtensionRegistry.register_extension("aws", AWSTemplateExtensionConfig) + # Register AWS DTO config as the typed provider_config class for TemplateDTO + # serialisation. AWSTemplateDTOConfig covers the fields that were previously + # split between the top-level TemplateDTO and the opaque metadata dict. + TemplateExtensionRegistry.register_extension("aws", AWSTemplateDTOConfig) if logger: logger.debug("AWS template extensions registered successfully") - # Remove print statement - should use structured logging except Exception as e: error_msg = f"Failed to register AWS template extensions: {e}" @@ -413,6 +442,43 @@ def get_aws_extension_defaults() -> dict: return default_config.to_template_defaults() +def register_aws_auth_strategies(logger: "Optional[LoggingPort]" = None) -> None: + """Register AWS authentication strategies with the auth registry. + + Registers the ``iam`` and ``cognito`` strategies so that ``AuthRegistry`` + can resolve them without server.py importing provider-specific classes. + + Args: + logger: Optional logger for registration messages + """ + try: + from orb.infrastructure.auth.registry import get_auth_registry + + registry = get_auth_registry() + + if not registry.is_registered("iam"): + from orb.providers.aws.auth.iam_strategy import IAMAuthStrategy + + registry.register_strategy("iam", IAMAuthStrategy) + if logger: + logger.debug("AWS IAM auth strategy registered") + + if not registry.is_registered("cognito"): + from orb.providers.aws.auth.cognito_strategy import CognitoAuthStrategy + + registry.register_strategy("cognito", CognitoAuthStrategy) + if logger: + logger.debug("AWS Cognito auth strategy registered") + + except ImportError as e: + if logger: + logger.warning("AWS auth strategies not available: %s", e) + except Exception as e: + if logger: + logger.error("Failed to register AWS auth strategies: %s", e, exc_info=True) + raise + + def initialize_aws_provider( template_factory: Optional[TemplateFactory] = None, logger: Optional["LoggingPort"] = None, @@ -433,16 +499,33 @@ def initialize_aws_provider( # Register AWS extensions register_aws_extensions(logger) + # Register AWS authentication strategies + register_aws_auth_strategies(logger) + # Register AWS template factory if provided if template_factory: register_aws_template_factory(template_factory, logger) # Register AWS CLI spec - from orb.domain.base.ports.provider_cli_spec_port import CLISpecRegistry + from orb.infrastructure.registry.cli_spec_registry import CLISpecRegistry from orb.providers.aws.cli.aws_cli_spec import AWSCLISpec CLISpecRegistry.register("aws", AWSCLISpec()) + # Register AWS HostFactory field-mapping adapter + from orb.infrastructure.scheduler.hostfactory.field_mapping_registry import ( + FieldMappingRegistry, + ) + from orb.providers.aws.scheduler.hostfactory_field_mapping import AWSFieldMapping + + FieldMappingRegistry.register("aws", AWSFieldMapping()) + + # Register AWS defaults loader + from orb.providers.aws.defaults_loader import AWSDefaultsLoader + from orb.providers.registry.defaults_loader_registry import DefaultsLoaderRegistry + + DefaultsLoaderRegistry.register("aws", AWSDefaultsLoader()) + if logger: logger.info("AWS provider initialization completed successfully") diff --git a/src/orb/providers/aws/scheduler/__init__.py b/src/orb/providers/aws/scheduler/__init__.py new file mode 100644 index 000000000..15d36c40e --- /dev/null +++ b/src/orb/providers/aws/scheduler/__init__.py @@ -0,0 +1 @@ +"""AWS scheduler adapters.""" diff --git a/src/orb/providers/aws/scheduler/hostfactory_field_mapping.py b/src/orb/providers/aws/scheduler/hostfactory_field_mapping.py new file mode 100644 index 000000000..5d8b595e1 --- /dev/null +++ b/src/orb/providers/aws/scheduler/hostfactory_field_mapping.py @@ -0,0 +1,77 @@ +"""AWS implementation of FieldMappingPort for the HostFactory scheduler.""" + +from orb.infrastructure.scheduler.hostfactory.field_mapping_port import FieldMappingPort + + +class AWSFieldMapping: + """AWS-specific field-mapping adapter for the HostFactory scheduler. + + Registers as ``"aws"`` in ``FieldMappingRegistry`` during provider + bootstrap (``providers/aws/registration.py``). + """ + + # AWS-specific HF field → internal field mappings. + # Generic mappings live in HostFactoryFieldMappings.MAPPINGS["generic"]. + _PROVIDER_MAPPINGS: dict[str, str] = { + # AWS VPC network configuration + "subnetId": "subnet_ids", + "subnetIds": "subnet_ids", + "securityGroupIds": "security_group_ids", + # AWS instance type configurations + "vmTypesOnDemand": "machine_types_ondemand", + "vmTypesPriority": "machine_types_priority", + "abisInstanceRequirements": "abis_instance_requirements", + # AWS pricing configurations + "percentOnDemand": "percent_on_demand", + "allocationStrategyOnDemand": "allocation_strategy_on_demand", + # AWS fleet configurations + "fleetRole": "fleet_role", + "spotFleetRequestExpiry": "spot_fleet_request_expiry", + "poolsCount": "pools_count", + # AWS instance configuration + "instanceProfile": "instance_profile", + "launchTemplateId": "launch_template_id", + "userDataScript": "user_data", + } + + def get_mappings(self) -> dict[str, str]: + """Return AWS-specific HF-field → internal-field name entries.""" + return dict(self._PROVIDER_MAPPINGS) + + def apply_defaults(self, mapped: dict) -> dict: + """Apply AWS-specific setdefault logic after field mapping. + + When ``launch_template_id`` is set the launch template already encodes + network configuration, so ``subnet_ids`` and ``security_group_ids`` + should not be defaulted to empty lists. + """ + mapped.setdefault("max_instances", 1) + mapped.setdefault("price_type", "ondemand") + mapped.setdefault("allocation_strategy", "lowestPrice") + if not mapped.get("launch_template_id"): + mapped.setdefault("subnet_ids", []) + mapped.setdefault("security_group_ids", []) + mapped.setdefault("tags", {}) + return mapped + + def derive_attributes(self, machine_type: str | None) -> dict[str, list[str]] | None: + """Build the HF ``attributes`` object from an EC2 instance type string. + + Returns ``None`` when *machine_type* is ``None`` or empty so callers + can fall back gracefully. + """ + if not machine_type: + return None + + from orb.providers.aws.utilities.ec2.instances import derive_cpu_ram_from_instance_type + + ncpus, nram = derive_cpu_ram_from_instance_type(machine_type) + return { + "type": ["String", "X86_64"], + "ncpus": ["Numeric", str(ncpus)], + "nram": ["Numeric", str(nram)], + } + + +# Verify the class satisfies the protocol at import time. +_: FieldMappingPort = AWSFieldMapping() # type: ignore[assignment] diff --git a/src/orb/providers/aws/strategy/aws_provider_adapter.py b/src/orb/providers/aws/strategy/aws_provider_adapter.py index 43ea141bc..9cdbaf5ea 100644 --- a/src/orb/providers/aws/strategy/aws_provider_adapter.py +++ b/src/orb/providers/aws/strategy/aws_provider_adapter.py @@ -12,7 +12,6 @@ ProviderResourceTag, ProviderResourceValidator, ProviderStateMapper, - ProviderType, ) @@ -120,9 +119,9 @@ def __init__(self, logger: LoggingPort) -> None: self._logger = logger @property - def provider_type(self) -> ProviderType: + def provider_type(self) -> str: """Get the provider type.""" - return ProviderType.AWS + return "aws" @property def state_mapper(self) -> ProviderStateMapper: @@ -143,7 +142,7 @@ def create_resource_identifier( raise ValueError(f"Invalid AWS {resource_type} identifier: {identifier}") return ProviderResourceIdentifier( - provider_type=ProviderType.AWS, + provider_type="aws", resource_type=resource_type, identifier=identifier, region=region, diff --git a/src/orb/providers/aws/strategy/aws_provider_strategy.py b/src/orb/providers/aws/strategy/aws_provider_strategy.py index b1027db6b..4b9e0b010 100644 --- a/src/orb/providers/aws/strategy/aws_provider_strategy.py +++ b/src/orb/providers/aws/strategy/aws_provider_strategy.py @@ -7,16 +7,17 @@ architecture and single responsibility principle. """ +import asyncio import time from typing import TYPE_CHECKING, Any, Callable, Optional from orb.domain.base.dependency_injection import injectable +from orb.domain.base.operation_outcome import Accepted, Completed, Failed, OperationOutcome from orb.domain.base.ports import LoggingPort from orb.domain.base.ports.configuration_port import ConfigurationPort # Import AWS-specific components from orb.providers.aws.configuration.config import AWSProviderConfig -from orb.providers.aws.exceptions.aws_exceptions import AWSConfigurationError from orb.providers.aws.infrastructure.aws_client import AWSClient from orb.providers.aws.services.capability_service import AWSCapabilityService from orb.providers.aws.services.handler_registry import AWSHandlerRegistry @@ -30,6 +31,7 @@ from orb.providers.aws.services.template_validation_service import AWSTemplateValidationService if TYPE_CHECKING: + from orb.domain.request.aggregate import Request from orb.monitoring.health import HealthCheck from orb.providers.aws.infrastructure.adapters.aws_provisioning_adapter import ( AWSProvisioningAdapter, @@ -409,8 +411,15 @@ async def _handle_describe_resource_instances( logic for discovering instances from resource IDs. This delegates to the correct handler's check_hosts_status method rather than using a generic service that lacks per-handler context. + + The handler returns a CheckHostsStatusResult containing both instance + details and a ProviderFulfilment verdict. The fulfilment is forwarded + in metadata so RequestStatusService can consume it without any + provider-specific logic. """ try: + from orb.domain.base.provider_fulfilment import ProviderFulfilment + resource_ids = operation.parameters.get("resource_ids", []) provider_api = operation.parameters.get("provider_api", "RunInstances") provider_api_value = ( @@ -442,25 +451,37 @@ async def _handle_describe_resource_instances( operation.context.get("request_id") if operation.context else None ) + requested_count = int(operation.parameters.get("requested_count") or 1) request = Request.create_new_request( request_type=RequestType.ACQUIRE, template_id=operation.parameters.get("template_id", "unknown"), - machine_count=1, + machine_count=requested_count, provider_type="aws", provider_name="aws-default", request_id=request_id, ) request.resource_ids = resource_ids - instance_details = handler.check_hosts_status(request) + # check_hosts_status makes blocking boto3 I/O calls (describe_fleet_instances, + # describe_instances, etc.). Running it directly in an async handler blocks + # the uvicorn event loop, which prevents uvicorn from accepting any further + # connections until the call completes. For large requests (e.g. 100 instances) + # this starvation causes all concurrent polls to fail with ConnectionError. + # Offloading to a thread pool executor keeps the event loop responsive. + check_result = await asyncio.to_thread(handler.check_hosts_status, request) + instance_details = check_result.instances + fulfilment: ProviderFulfilment = check_result.fulfilment metadata: dict[str, Any] = { "operation": "describe_resource_instances", "resource_ids": resource_ids, "provider_api": provider_api_value, "instance_count": len(instance_details), + # Forward the provider's fulfilment verdict to the application layer. + # RequestStatusService reads this as "provider_fulfilment" and trusts + # it exclusively — no count math or AWS-specific key inspection. + "provider_fulfilment": fulfilment, } - self._augment_capacity_metadata(metadata, provider_api_value, resource_ids) if not instance_details: self._logger.info("No instances found for resources: %s", resource_ids) @@ -477,7 +498,10 @@ async def _handle_describe_resource_instances( "DESCRIBE_RESOURCE_INSTANCES_ERROR", ) + # ------------------------------------------------------------------------- # Infrastructure discovery methods (delegated to service) + # ------------------------------------------------------------------------- + def discover_infrastructure(self, provider_config: dict[str, Any]) -> dict[str, Any]: """Discover AWS infrastructure for provider.""" return self._get_infrastructure_service().discover_infrastructure(provider_config) @@ -559,7 +583,13 @@ def register_health_checks(self, health_check: HealthCheck) -> None: return from orb.providers.aws.health import register_aws_health_checks - register_aws_health_checks(health_check, self.aws_client) + storage_strategy = "json" + if self._config_port is not None: + try: + storage_strategy = self._config_port.get_storage_strategy() + except Exception: + pass + register_aws_health_checks(health_check, self.aws_client, storage_strategy) def cleanup(self) -> None: """Clean up AWS provider resources.""" @@ -571,100 +601,6 @@ def cleanup(self) -> None: except Exception as e: self._logger.warning("Failed during AWS provider cleanup: %s", e, exc_info=True) - def _augment_capacity_metadata( - self, metadata: dict[str, Any], provider_api: str, resource_ids: list[str] - ) -> None: - """Add fleet capacity fulfillment data to metadata for fleet-based providers.""" - if not resource_ids or not self.aws_client: - return - try: - resource_id = resource_ids[0] - capacity_fetchers: dict[str, Callable[[str], Optional[dict[str, Any]]]] = { - "EC2Fleet": self._fetch_ec2_fleet_capacity, - "SpotFleet": self._fetch_spot_fleet_capacity, - "ASG": self._fetch_asg_capacity, - } - fetcher = capacity_fetchers.get(provider_api) - if fetcher: - result = fetcher(resource_id) - if result is not None: - metadata["fleet_capacity_fulfilment"] = result - except Exception as e: - self._logger.warning("Failed to augment capacity metadata: %s", e) - - def _fetch_ec2_fleet_capacity(self, resource_id: str) -> Optional[dict[str, Any]]: - """Fetch capacity fulfillment data for an EC2Fleet resource.""" - if self.aws_client is None: - raise AWSConfigurationError( - "aws_client must be injected before calling _fetch_ec2_fleet_capacity" - ) - response = self.aws_client.ec2_client.describe_fleets(FleetIds=[resource_id]) - fleets = response.get("Fleets", []) - if not fleets: - return None - fleet = fleets[0] - spec = fleet.get("TargetCapacitySpecification") or {} - target = spec.get("TotalTargetCapacity") - fulfilled = fleet.get("FulfilledCapacity") or 0 - fleet_type_val = (fleet.get("Type") or "maintain").lower() - return { - "target_capacity_units": target, - "fulfilled_capacity_units": fulfilled, - "provisioned_instance_count": int(fulfilled), - "state": fleet.get("FleetState"), - "fleet_type": fleet_type_val, - "fulfillment_final": fleet_type_val == "instant", - } - - def _fetch_spot_fleet_capacity(self, resource_id: str) -> Optional[dict[str, Any]]: - """Fetch capacity fulfillment data for a SpotFleet resource.""" - if self.aws_client is None: - raise AWSConfigurationError( - "aws_client must be injected before calling _fetch_spot_fleet_capacity" - ) - response = self.aws_client.ec2_client.describe_spot_fleet_requests( - SpotFleetRequestIds=[resource_id] - ) - configs = response.get("SpotFleetRequestConfigs", []) - if not configs: - return None - cfg = configs[0].get("SpotFleetRequestConfig") or {} - fulfilled = cfg.get("FulfilledCapacity") or 0 - return { - "target_capacity_units": cfg.get("TargetCapacity"), - "fulfilled_capacity_units": fulfilled, - "provisioned_instance_count": int(fulfilled), - "state": configs[0].get("SpotFleetRequestState"), - "fleet_type": (cfg.get("Type") or "request").lower(), - } - - def _fetch_asg_capacity(self, resource_id: str) -> Optional[dict[str, Any]]: - """Fetch capacity fulfillment data for an Auto Scaling Group resource.""" - if self.aws_client is None: - raise AWSConfigurationError( - "aws_client must be injected before calling _fetch_asg_capacity" - ) - response = self.aws_client.autoscaling_client.describe_auto_scaling_groups( - AutoScalingGroupNames=[resource_id] - ) - groups = response.get("AutoScalingGroups", []) - if not groups: - return None - group = groups[0] - instances = group.get("Instances") or [] - fulfilled = sum( - int(inst.get("WeightedCapacity", 1)) - for inst in instances - if inst.get("LifecycleState") == "InService" - ) - provisioned = sum(1 for inst in instances if inst.get("LifecycleState") == "InService") - return { - "target_capacity_units": int(group.get("DesiredCapacity") or 0), - "fulfilled_capacity_units": fulfilled, - "provisioned_instance_count": provisioned, - "state": group.get("Status"), - } - async def _handle_resolve_image(self, operation: ProviderOperation) -> ProviderResult: """Handle image resolution using registry-based service.""" try: @@ -712,6 +648,182 @@ def _create_image_resolution_service(self): logger=self._logger, ) + # ------------------------------------------------------------------ + # Typed provisioning interface — OperationOutcome + # ------------------------------------------------------------------ + + async def acquire(self, request: "Request") -> OperationOutcome: + """Submit an acquisition request to AWS. + + AWS provider operations are asynchronous: the API call (EC2Fleet, + SpotFleet, RunInstances, ASG) returns a request/fleet ID immediately + while instances transition through ``pending``. The outcome is + therefore always ``Accepted`` on success. + + Args: + request: Domain request describing resources to acquire. + + Returns: + ``Accepted`` with pending instance IDs on success. + ``Failed`` on provider rejection or configuration error. + """ + try: + # Build operation using the strategy-layer types already imported at the + # module level (ProviderOperation / ProviderOperationType from + # orb.providers.base.strategy). These are what execute_operation() expects. + operation = ProviderOperation( + operation_type=ProviderOperationType.CREATE_INSTANCES, + parameters={ + "template_config": {}, + "count": request.requested_count, + "request_id": str(request.request_id), + "request_metadata": dict(request.metadata), + }, + context={ + "correlation_id": str(request.request_id), + "request_id": str(request.request_id), + "dry_run": request.metadata.get("dry_run", False), + }, + ) + result = await self.execute_operation(operation) + + if not result.success: + return Failed( + error=result.error_message or "AWS acquire failed", + recoverable=False, + ) + + resource_ids: list[str] = (result.data or {}).get("resource_ids", []) + request_id = str(request.request_id) + + self._logger.info( + "AWS acquire accepted: request_id=%s, pending_resource_ids=%s", + request_id, + resource_ids, + ) + return Accepted( + request_id=request_id, + pending_resource_ids=resource_ids, + metadata=result.metadata or {}, + ) + + except Exception as exc: + self._logger.error("AWS acquire failed: %s", exc, exc_info=True) + return Failed(error=str(exc), recoverable=False) + + async def return_machines(self, machine_ids: list[str], request: "Request") -> OperationOutcome: + """Submit a return (termination) request to AWS. + + AWS terminates asynchronously — ``TerminateInstances`` returns + immediately while instances move through ``shutting-down``. The + outcome is therefore ``Accepted`` with the terminating IDs. + + Args: + machine_ids: EC2 instance IDs to terminate. + request: Domain request providing context. + + Returns: + ``Accepted`` with terminating instance IDs on success. + ``Failed`` on provider rejection or configuration error. + """ + try: + operation = ProviderOperation( + operation_type=ProviderOperationType.TERMINATE_INSTANCES, + parameters={ + "instance_ids": machine_ids, + "request_id": str(request.request_id), + "template_id": request.template_id, + "provider_api": request.provider_api or "RunInstances", + }, + context={ + "correlation_id": str(request.request_id), + "request_id": str(request.request_id), + }, + ) + result = await self.execute_operation(operation) + + if not result.success: + return Failed( + error=result.error_message or "AWS return_machines failed", + recoverable=False, + ) + + self._logger.info( + "AWS termination accepted: request_id=%s, terminating=%s", + request.request_id, + machine_ids, + ) + return Accepted( + request_id=str(request.request_id), + pending_resource_ids=list(machine_ids), + metadata=result.metadata or {}, + ) + + except Exception as exc: + self._logger.error("AWS return_machines failed: %s", exc, exc_info=True) + return Failed(error=str(exc), recoverable=False) + + async def get_status(self, resource_ids: list[str], request: "Request") -> OperationOutcome: + """Query AWS instance status for previously submitted resources. + + Returns ``Completed`` only when *all* instances have reached a + terminal state (``running`` for acquire, ``terminated`` for return). + Returns ``Accepted`` (still in-progress) otherwise. + + Args: + resource_ids: EC2 instance or resource IDs to check. + request: Domain request providing context. + + Returns: + ``Completed`` when all instances are terminal. + ``Accepted`` when one or more instances are still transitioning. + ``Failed`` when all instances failed or a hard error occurred. + """ + try: + operation = ProviderOperation( + operation_type=ProviderOperationType.GET_INSTANCE_STATUS, + parameters={ + "instance_ids": resource_ids, + "template_id": request.template_id, + "provider_api": request.provider_api or "RunInstances", + }, + context={ + "correlation_id": str(request.request_id), + "request_id": str(request.request_id), + }, + ) + result = await self.execute_operation(operation) + + if not result.success: + return Failed( + error=result.error_message or "AWS get_status failed", + recoverable=True, + ) + + instances: list[dict[str, Any]] = (result.data or {}).get("instances", []) + terminal_states = frozenset({"running", "terminated", "stopped", "failed"}) + non_terminal = [ + inst for inst in instances if inst.get("status", "") not in terminal_states + ] + + if non_terminal: + still_pending = [inst.get("instance_id", "") for inst in non_terminal] + return Accepted( + request_id=str(request.request_id), + pending_resource_ids=still_pending, + metadata=result.metadata or {}, + ) + + completed_ids = [inst.get("instance_id", "") for inst in instances] + return Completed( + resource_ids=completed_ids, + metadata=result.metadata or {}, + ) + + except Exception as exc: + self._logger.error("AWS get_status failed: %s", exc, exc_info=True) + return Failed(error=str(exc), recoverable=True) + def __str__(self) -> str: """Return string representation for debugging.""" return f"AWSProviderStrategy(region={self._aws_config.region}, initialized={self._initialized})" diff --git a/src/orb/providers/aws/value_objects.py b/src/orb/providers/aws/value_objects.py new file mode 100644 index 000000000..d9c0cea71 --- /dev/null +++ b/src/orb/providers/aws/value_objects.py @@ -0,0 +1,19 @@ +"""Public AWS provider value objects. + +Re-exports AWS-specific value objects from their canonical locations so that +callers outside the internal sub-packages can use a stable, shallow import path: + + from orb.providers.aws.value_objects import AWSAllocationStrategy +""" + +from orb.providers.aws.domain.template.value_objects import ( + CANONICAL_ALLOCATION_STRATEGIES, + AWSAllocationStrategy, + normalise_allocation_strategy, +) + +__all__: list[str] = [ + "AWSAllocationStrategy", + "CANONICAL_ALLOCATION_STRATEGIES", + "normalise_allocation_strategy", +] diff --git a/src/orb/providers/base/strategy/base_provider_strategy.py b/src/orb/providers/base/strategy/base_provider_strategy.py index 09078bfc2..bba01542c 100644 --- a/src/orb/providers/base/strategy/base_provider_strategy.py +++ b/src/orb/providers/base/strategy/base_provider_strategy.py @@ -1,10 +1,14 @@ """Base provider strategy implementing ProviderPort.""" -from abc import ABC -from typing import Any +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any +from orb.domain.base.operation_outcome import OperationOutcome from orb.domain.base.ports.provider_port import ProviderPort +if TYPE_CHECKING: + from orb.domain.request.aggregate import Request + class BaseProviderStrategy(ProviderPort, ABC): """Base class for all provider strategies implementing ProviderPort.""" @@ -26,3 +30,58 @@ def get_provider_info(self) -> dict[str, Any]: @classmethod def get_defaults_config(cls) -> dict: return {} + + # ------------------------------------------------------------------ + # Typed provisioning interface — returns OperationOutcome + # ------------------------------------------------------------------ + + @abstractmethod + async def acquire(self, request: "Request") -> OperationOutcome: + """Submit an acquisition request to the provider. + + The provider *accepts* the request and returns immediately. Because + most cloud providers (AWS EC2Fleet, SpotFleet, …) are async by nature + the outcome is typically ``Accepted`` rather than ``Completed``. + Callers must poll via :meth:`get_status` until a terminal outcome. + + Args: + request: Domain request describing what resources to acquire. + + Returns: + ``Accepted`` — provider accepted, instances pending. + ``Completed`` — provider fulfilled synchronously (rare, e.g. dry-run). + ``Failed`` — provider rejected or hard error. + """ + + @abstractmethod + async def return_machines(self, machine_ids: list[str], request: "Request") -> OperationOutcome: + """Submit a return (termination) request to the provider. + + AWS terminates asynchronously — the API call returns immediately while + instances transition through ``shutting-down``. The outcome is + therefore ``Accepted`` with the terminating IDs as ``pending_resource_ids``. + + Args: + machine_ids: Provider-side instance/resource IDs to terminate. + request: Domain request providing context (provider_name, template, …). + + Returns: + ``Accepted`` — termination accepted, instances shutting down. + ``Completed`` — terminated synchronously (rare / mock). + ``Failed`` — provider rejected or hard error. + """ + + @abstractmethod + async def get_status(self, resource_ids: list[str], request: "Request") -> OperationOutcome: + """Query the current status of previously submitted resources. + + Args: + resource_ids: Provider-side resource IDs to check. + request: Domain request providing context. + + Returns: + ``Completed`` — all resources reached a terminal success state. + ``Accepted`` — resources still in a non-terminal state. + ``RequiresFollowUp`` — a specific follow-up action is needed. + ``Failed`` — all resources failed or a hard error occurred. + """ diff --git a/src/orb/providers/registration.py b/src/orb/providers/registration.py index cfd58e634..b92d4fb89 100644 --- a/src/orb/providers/registration.py +++ b/src/orb/providers/registration.py @@ -1,20 +1,127 @@ -"""Provider registration functions.""" +"""Provider registration functions. +To add a new provider: + 1. Add its name to ``_REGISTERED_PROVIDERS`` below (one line). + 2. Create ``src/orb/providers//registration.py`` that exposes: + - ``register__provider(registry)`` – registers strategy + factories + - ``initialize__provider(container)`` – wires DI services (optional) -def register_all_provider_types() -> None: - """Register all available provider types.""" +That is the only edit outside the new provider package. +""" + +from __future__ import annotations + +import importlib +import importlib.util +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from orb.infrastructure.di.container import DIContainer + +# --------------------------------------------------------------------------- +# Central provider list – the single line to edit when adding a new provider. +# --------------------------------------------------------------------------- +_REGISTERED_PROVIDERS: list[str] = ["aws"] + + +def register_all_providers(container: "DIContainer | None" = None) -> None: + """Register all providers listed in ``_REGISTERED_PROVIDERS``. + + For each provider name ``n`` this function: + 1. Imports ``orb.providers..registration`` (skips silently on ImportError + so that optional provider extras are handled gracefully). + 2. Calls ``register__provider(registry)`` to register the strategy and + supporting factories with the global provider registry. + 3. If *container* is given, calls ``initialize__provider(container)`` + when that function exists in the module. + + Args: + container: Optional DI container. When supplied, per-provider DI + wiring is performed in the same call; when omitted only the + registry-level registration is performed. + """ from orb.providers.registry import get_provider_registry registry = get_provider_registry() - # Register AWS provider - from orb.providers.aws.registration import register_aws_provider + for name in _REGISTERED_PROVIDERS: + module_path = f"orb.providers.{name}.registration" + if importlib.util.find_spec(module_path) is None: + continue + try: + mod = importlib.import_module(module_path) + except ImportError: + continue + + # Registry-level registration + register_fn = getattr(mod, f"register_{name}_provider", None) + if register_fn is not None: + register_fn(registry) + + # DI-level initialisation (only when a container is supplied) + if container is not None: + init_fn = getattr(mod, f"initialize_{name}_provider", None) + if init_fn is not None: + init_fn(container) + + +# --------------------------------------------------------------------------- +# Deprecated aliases – kept for backward compatibility with existing callers. +# --------------------------------------------------------------------------- + + +def register_all_provider_types() -> None: + """Register all available provider types. + + Deprecated: use ``register_all_providers()`` instead. Kept as a + backward-compatible alias so existing callers continue to work without + modification. + """ + register_all_providers(container=None) + + +def register_all_provider_cli_specs() -> None: + """Register CLI argument specs for all available providers. + + Lightweight bootstrap that only registers CLI specs so that + ``build_parser`` can call it before any application context exists. + + Deprecated: ``register_all_providers()`` now handles CLI spec registration + as part of ``initialize__provider``. This alias is retained so that + ``cli/args.py`` and other early-bootstrap callers continue to work. + """ + from orb.infrastructure.registry.cli_spec_registry import CLISpecRegistry + + try: + from orb.providers.aws.cli.aws_cli_spec import AWSCLISpec + + if CLISpecRegistry.get("aws") is None: + CLISpecRegistry.register("aws", AWSCLISpec()) + except ImportError: + pass # [aws] extra not installed; AWS CLI spec unavailable + + +def register_all_defaults_loaders() -> None: + """Register defaults loaders for all available providers. + + Lightweight bootstrap that only registers ``ProviderDefaultsLoaderPort`` + implementations so that ``ConfigurationLoader._load_strategy_defaults`` can + call it before a full application context has been set up. + + Deprecated: ``register_all_providers()`` now handles defaults-loader + registration as part of ``initialize__provider``. This alias is + retained so that ``config/loader.py`` and other early-bootstrap callers + continue to work. + """ + from orb.providers.registry.defaults_loader_registry import DefaultsLoaderRegistry - register_aws_provider(registry) + if DefaultsLoaderRegistry.get("aws") is None: + try: + from orb.providers.aws.defaults_loader import AWSDefaultsLoader - # Future providers would be added here - # register_provider1_provider(registry) - # register_provider2_provider(registry) + DefaultsLoaderRegistry.register("aws", AWSDefaultsLoader()) + except ImportError: + pass # [aws] extra not installed; AWS defaults loader unavailable def register_fallback_provider( diff --git a/src/orb/providers/registry/__init__.py b/src/orb/providers/registry/__init__.py index 72417c075..bd3f0a606 100644 --- a/src/orb/providers/registry/__init__.py +++ b/src/orb/providers/registry/__init__.py @@ -1,5 +1,6 @@ """Provider registry package - re-exports all public symbols for backward compatibility.""" +from orb.providers.registry.defaults_loader_registry import DefaultsLoaderRegistry from orb.providers.registry.provider_registry import ProviderRegistry, get_provider_registry from orb.providers.registry.types import ( ProviderFactoryInterface, @@ -8,6 +9,7 @@ ) __all__ = [ + "DefaultsLoaderRegistry", "ProviderRegistry", "get_provider_registry", "ProviderFactoryInterface", diff --git a/src/orb/providers/registry/defaults_loader_registry.py b/src/orb/providers/registry/defaults_loader_registry.py new file mode 100644 index 000000000..bea0e8fcd --- /dev/null +++ b/src/orb/providers/registry/defaults_loader_registry.py @@ -0,0 +1,54 @@ +"""Registry mapping provider types to ProviderDefaultsLoaderPort instances.""" + +from __future__ import annotations + +from orb.domain.base.ports.provider_defaults_loader_port import ProviderDefaultsLoaderPort + + +class DefaultsLoaderRegistry: + """Simple class-variable registry mapping provider type strings to + ``ProviderDefaultsLoaderPort`` implementations. + + Follows the same lightweight pattern as ``CLISpecRegistry`` and + ``FieldMappingRegistry``. + + Usage:: + + # During provider bootstrap: + DefaultsLoaderRegistry.register("aws", AWSDefaultsLoader()) + + # At call site: + for provider_type, loader in DefaultsLoaderRegistry.all().items(): + defaults = loader.load_defaults() + """ + + _loaders: dict[str, ProviderDefaultsLoaderPort] = {} + + @classmethod + def register(cls, provider_type: str, loader: ProviderDefaultsLoaderPort) -> None: + """Register a defaults loader for *provider_type*. + + Registration is idempotent — re-registering the same provider type + silently overwrites the previous entry. + """ + cls._loaders[provider_type] = loader + + @classmethod + def get(cls, provider_type: str) -> ProviderDefaultsLoaderPort | None: + """Return the loader for *provider_type*, or ``None`` if not registered.""" + return cls._loaders.get(provider_type) + + @classmethod + def all(cls) -> dict[str, ProviderDefaultsLoaderPort]: + """Return all registered loaders keyed by provider type.""" + return dict(cls._loaders) + + @classmethod + def registered_providers(cls) -> list[str]: + """Return all registered provider type strings.""" + return list(cls._loaders.keys()) + + @classmethod + def clear(cls) -> None: + """Remove all registrations (primarily for use in tests).""" + cls._loaders.clear() diff --git a/src/orb/providers/registry/provider_registry.py b/src/orb/providers/registry/provider_registry.py index aeda55e18..24b51d266 100644 --- a/src/orb/providers/registry/provider_registry.py +++ b/src/orb/providers/registry/provider_registry.py @@ -1,9 +1,16 @@ """Provider Registry - Registry pattern for provider strategy factories.""" import importlib +import re import threading from typing import Any, Callable, List, Optional +# Only allow simple snake_case identifiers as provider types to prevent +# module-injection via crafted provider_type strings (e.g. containing dots +# or path-traversal sequences) that would be interpolated directly into the +# dynamic importlib.import_module() call. +_VALID_PROVIDER_TYPE_RE = re.compile(r"^[a-z][a-z0-9_]*$") + from orb.domain.base.exceptions import ConfigurationError from orb.domain.base.ports.configuration_port import ConfigurationPort from orb.domain.base.ports.provider_registry_port import ProviderRegistryPort @@ -143,6 +150,9 @@ def ensure_provider_type_registered(self, provider_type: str) -> bool: return True # Try to dynamically import and register + if not _VALID_PROVIDER_TYPE_RE.match(provider_type): + raise ValueError(f"Invalid provider type: {provider_type!r}") + module_name = f"orb.providers.{provider_type}.registration" try: if self._logger: @@ -208,6 +218,9 @@ def ensure_provider_instance_registered_from_config(self, provider_instance: Any try: provider_type = provider_instance.type + if not _VALID_PROVIDER_TYPE_RE.match(provider_type): + raise ValueError(f"Invalid provider type: {provider_type!r}") + if self._logger: self._logger.debug("Registering provider instance: %s", provider_instance.name) @@ -238,6 +251,7 @@ def register( # type: ignore[override] resolver_factory: Optional[Callable] = None, validator_factory: Optional[Callable] = None, strategy_class: Optional[type] = None, + default_api: Optional[str] = None, **kwargs: Any, ) -> None: """Register provider type - implements abstract method.""" @@ -249,10 +263,28 @@ def register( # type: ignore[override] resolver_factory=resolver_factory, validator_factory=validator_factory, strategy_class=strategy_class, + default_api=default_api, ) except ValueError as e: raise ConfigurationError(str(e)) + def get_default_api(self, provider_type: str) -> Optional[str]: + """Return the default API name for the given provider type, or None if not set. + + Args: + provider_type: Type identifier for the provider (e.g., 'aws') + + Returns: + Default API string from registration, or None if not registered / not set. + """ + try: + registration = self._get_type_registration(provider_type) + if isinstance(registration, ProviderRegistration): + return registration.default_api + except (ValueError, KeyError): + pass + return None + def register_provider( self, provider_type: str, @@ -261,6 +293,7 @@ def register_provider( resolver_factory: Optional[Callable] = None, validator_factory: Optional[Callable] = None, strategy_class: Optional[type] = None, + default_api: Optional[str] = None, ) -> None: """ Register a provider with its factory functions - backward compatibility method. @@ -271,6 +304,8 @@ def register_provider( config_factory: Factory function to create provider configuration resolver_factory: Optional factory for template resolver validator_factory: Optional factory for template validator + strategy_class: Optional provider strategy class + default_api: Optional default API name contributed by this provider Raises: ValueError: If provider_type is already registered @@ -282,6 +317,7 @@ def register_provider( resolver_factory, validator_factory, strategy_class=strategy_class, + default_api=default_api, ) def register_provider_instance( @@ -853,6 +889,7 @@ def _create_registration( additional_factories.get("resolver_factory"), additional_factories.get("validator_factory"), strategy_class=additional_factories.get("strategy_class"), + default_api=additional_factories.get("default_api"), ) @staticmethod diff --git a/src/orb/providers/registry/types.py b/src/orb/providers/registry/types.py index fdb74a06a..9b19373ea 100644 --- a/src/orb/providers/registry/types.py +++ b/src/orb/providers/registry/types.py @@ -33,6 +33,7 @@ def __init__( resolver_factory: Optional[Callable] = None, validator_factory: Optional[Callable] = None, strategy_class: Optional[type] = None, + default_api: Optional[str] = None, ) -> None: """Initialize the instance.""" super().__init__( @@ -45,3 +46,4 @@ def __init__( self.resolver_factory = resolver_factory self.validator_factory = validator_factory self.strategy_class = strategy_class + self.default_api = default_api diff --git a/src/orb/sdk/client.py b/src/orb/sdk/client.py index 60a27a1c5..3ec5684e9 100644 --- a/src/orb/sdk/client.py +++ b/src/orb/sdk/client.py @@ -477,12 +477,28 @@ async def request_machines(self, template_id: str, count: int, **kwargs) -> dict return scheduler.format_request_response(raw) return raw - async def get_request_status(self, request_ids: list, **kwargs) -> dict: - """Get status for one or more requests via GetRequestStatusOrchestrator.""" + async def get_request_status( + self, + request_ids: "list | None" = None, + *, + request_id: "str | None" = None, + **kwargs, + ) -> dict: + """Get status for one or more requests via GetRequestStatusOrchestrator. + + Accepts either ``request_ids`` (list) or ``request_id`` (str, singular alias + for single-ID callers). Both are merged into a single list before dispatch. + """ if not self._initialized: raise SDKError("SDK not initialized. Use as async context manager.") assert self._container is not None + # Resolve the list of IDs from either parameter name. + ids: list = list(request_ids) if request_ids is not None else [] + if request_id is not None: + if request_id not in ids: + ids.append(request_id) + from orb.application.ports.scheduler_port import SchedulerPort from orb.application.services.orchestration.dtos import GetRequestStatusInput from orb.application.services.orchestration.get_request_status import ( @@ -492,7 +508,7 @@ async def get_request_status(self, request_ids: list, **kwargs) -> dict: orchestrator = self._container.get(GetRequestStatusOrchestrator) result = await orchestrator.execute( GetRequestStatusInput( - request_ids=list(request_ids), + request_ids=ids, all_requests=kwargs.get("all_requests", False), verbose=kwargs.get("verbose", False), ) diff --git a/src/orb/sdk/config.py b/src/orb/sdk/config.py index bc718a708..eca1eb98f 100644 --- a/src/orb/sdk/config.py +++ b/src/orb/sdk/config.py @@ -70,7 +70,19 @@ def from_dict(cls, config: dict[str, Any]) -> "SDKConfig": for key, value in config.items(): if key in known_fields: - sdk_config[key] = value + # ``scheduler`` is a plain string override in SDKConfig (e.g. "default" + # or "hostfactory"). When loading from an ORB config.json the top-level + # ``scheduler`` key is the full scheduler sub-config dict + # ({"type": "hostfactory", "config_root": "..."}). Ingesting that dict + # as the string override causes ConfigurationManager.override_scheduler_strategy + # to store a dict, which then propagates as the scheduler_type into the + # registry lookup and fails with "unhashable type: 'dict'". + if key == "scheduler" and isinstance(value, dict): + # The ORB config scheduler object is not an SDK string override; + # skip it so the scheduler type is resolved from the config file. + custom_config[key] = value + else: + sdk_config[key] = value else: custom_config[key] = value diff --git a/src/orb/sdk/protocols.py b/src/orb/sdk/protocols.py index d9b380be4..b4c4dfee1 100644 --- a/src/orb/sdk/protocols.py +++ b/src/orb/sdk/protocols.py @@ -48,7 +48,13 @@ async def refresh_templates(self, **kwargs: Any) -> dict[str, Any]: # type: ign pass # --- Request operations --- - async def get_request_status(self, request_ids: list[str], **kwargs: Any) -> dict[str, Any]: # type: ignore[return] + async def get_request_status( + self, + request_ids: "list[str] | None" = None, + *, + request_id: "str | None" = None, + **kwargs: Any, + ) -> dict[str, Any]: # type: ignore[return] pass async def get_request(self, *, request_id: str, **kwargs: Any) -> dict[str, Any]: # type: ignore[return] diff --git a/tests/conftest.py b/tests/conftest.py index b80e9cab4..f6d35b62f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,25 @@ def pytest_addoption(parser): "--run-aws", action="store_true", default=False, - help="Run tests requiring real AWS credentials", + help="Alias for --live (backward compat): run tests requiring real AWS credentials", + ) + parser.addoption( + "--live", + action="store_true", + default=False, + help="Enable tests under tests/providers//live/ (require real credentials)", + ) + parser.addoption( + "--no-mocked", + action="store_true", + default=False, + help="Skip mocked/moto tests under tests/providers//moto/", + ) + parser.addoption( + "--provider", + action="store", + default=None, + help="Filter to tests under tests/providers// only (e.g. --provider aws)", ) parser.addoption( "--keep-logs", @@ -18,15 +36,74 @@ def pytest_addoption(parser): ) +def _is_live_test(item) -> bool: + """Return True when the test lives under a providers//live/ subtree.""" + path = str(item.fspath) + return "/providers/" in path and "/live/" in path + + +def _is_moto_test(item) -> bool: + """Return True when the test lives under a providers//moto/ subtree.""" + path = str(item.fspath) + return "/providers/" in path and "/moto/" in path + + def pytest_collection_modifyitems(config, items): - if config.getoption("--run-aws"): - return - if any("onaws" in str(a) for a in config.args): - return - skip = pytest.mark.skip(reason="requires real AWS credentials — pass --run-aws to run") + live_enabled = config.getoption("--live") or config.getoption("--run-aws") + no_mocked = config.getoption("--no-mocked") + provider_filter = config.getoption("--provider") + + # Also honour legacy path-based live detection (e.g. pytest tests/providers/aws/live) + explicit_live = any("live" in str(a) or "onaws" in str(a) for a in config.args) + if explicit_live: + live_enabled = True + + skip_live = pytest.mark.skip( + reason="requires real credentials — pass --live (or --run-aws) to run" + ) + skip_mocked = pytest.mark.skip(reason="moto tests skipped — remove --no-mocked to run") + skip_provider = pytest.mark.skip(reason=f"filtered to --provider {provider_filter}") + + # Auto-skip AWS provider tests when boto3 is not installed + try: + import boto3 as _boto3 # noqa: F401 + + aws_available = True + except ImportError: + aws_available = False + + skip_no_aws = pytest.mark.skip(reason="[aws] extra not installed — boto3 unavailable") + for item in items: - if item.get_closest_marker("aws") and not item.get_closest_marker("provider_contract"): - item.add_marker(skip) + path = str(item.fspath) + + # Auto-skip AWS provider tests when [aws] extra is not installed + if not aws_available and "/providers/aws/" in path: + item.add_marker(skip_no_aws) + + # Provider filter + if provider_filter and f"/providers/{provider_filter}" not in path: + if "/providers/" in path: + item.add_marker(skip_provider) + + # Live gate + if _is_live_test(item) and not live_enabled: + item.add_marker(skip_live) + continue + + # Moto gate + if _is_moto_test(item) and no_mocked: + item.add_marker(skip_mocked) + continue + + # Legacy marker-based gate (non-provider-tree tests marked @pytest.mark.aws) + if ( + item.get_closest_marker("aws") + and not item.get_closest_marker("provider_contract") + and not live_enabled + and not _is_live_test(item) + ): + item.add_marker(skip_live) import json @@ -59,7 +136,12 @@ def mock_aws(): # type: ignore[misc] yield -import boto3 +try: + import boto3 + + AWS_AVAILABLE = True +except ImportError: + AWS_AVAILABLE = False from orb.config.manager import ConfigurationManager from orb.config.schemas.app_schema import AppConfig @@ -223,18 +305,24 @@ def aws_mocks(): @pytest.fixture def ec2_client(aws_mocks): """Create a mocked EC2 client.""" + if not AWS_AVAILABLE: + pytest.skip("boto3 not installed — install [aws] extra") return boto3.client("ec2", region_name="us-east-1") @pytest.fixture def autoscaling_client(aws_mocks): """Create a mocked Auto Scaling client.""" + if not AWS_AVAILABLE: + pytest.skip("boto3 not installed — install [aws] extra") return boto3.client("autoscaling", region_name="us-east-1") @pytest.fixture def ssm_client(aws_mocks): """Create a mocked SSM client.""" + if not AWS_AVAILABLE: + pytest.skip("boto3 not installed — install [aws] extra") return boto3.client("ssm", region_name="us-east-1") diff --git a/tests/contract/test_default_contract.py b/tests/contract/test_default_contract.py index 46000454a..aff203032 100644 --- a/tests/contract/test_default_contract.py +++ b/tests/contract/test_default_contract.py @@ -1,7 +1,7 @@ """Default scheduler boundary contract tests. Validates that every response ORB emits through the default scheduler -interface conforms to the schemas defined in tests/onaws/plugin_io_schemas.py. +interface conforms to the schemas defined in tests/providers/aws/live/plugin_io_schemas.py. """ import re @@ -17,7 +17,7 @@ except ImportError: pytest.skip("jsonschema not installed", allow_module_level=True) -from tests.onaws.plugin_io_schemas import ( +from tests.providers.aws.live.plugin_io_schemas import ( expected_get_available_templates_schema_default, expected_request_machines_schema_default, expected_request_status_schema_default, diff --git a/tests/contract/test_hf_contract.py b/tests/contract/test_hf_contract.py index a56383757..9624c8d2a 100644 --- a/tests/contract/test_hf_contract.py +++ b/tests/contract/test_hf_contract.py @@ -1,7 +1,7 @@ """HostFactory plugin boundary contract tests (Boundary A). Validates that every response ORB emits through the HF plugin interface -conforms to the schemas defined in tests/onaws/plugin_io_schemas.py. +conforms to the schemas defined in tests/providers/aws/live/plugin_io_schemas.py. The strictest contract is request_machines: additionalProperties=False means any extra field added to that response immediately breaks HF. @@ -23,7 +23,7 @@ except ImportError: pytest.skip("jsonschema not installed", allow_module_level=True) -from tests.onaws.plugin_io_schemas import ( +from tests.providers.aws.live.plugin_io_schemas import ( expected_get_available_templates_schema_hostfactory, expected_request_machines_schema_hostfactory, expected_request_status_schema_hostfactory, diff --git a/tests/integration/test_clean_architecture_integration.py b/tests/integration/test_clean_architecture_integration.py index fc1fdafba..ad7b92490 100644 --- a/tests/integration/test_clean_architecture_integration.py +++ b/tests/integration/test_clean_architecture_integration.py @@ -6,9 +6,9 @@ from orb.application.services.template_defaults_service import TemplateDefaultsService from orb.domain.base.ports.logging_port import LoggingPort -from orb.domain.template.extensions import TemplateExtensionRegistry from orb.domain.template.factory import TemplateFactory from orb.domain.template.template_aggregate import Template +from orb.infrastructure.registry.template_extension_registry import TemplateExtensionRegistry from orb.providers.aws.configuration.template_extension import AWSTemplateExtensionConfig @@ -85,22 +85,36 @@ def template_defaults_service(self, mock_config_manager, mock_logger, template_f def test_aws_extension_registration(self): """Test that AWS extensions are properly registered.""" - # Clear registry for clean test - TemplateExtensionRegistry.clear_registry() - - # Register AWS extension - TemplateExtensionRegistry.register_extension("aws", AWSTemplateExtensionConfig) - - # Verify registration - assert TemplateExtensionRegistry.has_extension("aws") - assert TemplateExtensionRegistry.get_extension_class("aws") == AWSTemplateExtensionConfig + # Snapshot both class and instance registries before mutating them so we + # can restore them afterwards. clear_registry() + selective re-register + # must not leak state into subsequent tests. + saved_extensions = dict(TemplateExtensionRegistry._extensions) + saved_instances = dict(TemplateExtensionRegistry._extension_instances) + try: + # Clear registry for clean test + TemplateExtensionRegistry.clear_registry() + + # Register AWS extension + TemplateExtensionRegistry.register_extension("aws", AWSTemplateExtensionConfig) + + # Verify registration + assert TemplateExtensionRegistry.has_extension("aws") + assert ( + TemplateExtensionRegistry.get_extension_class("aws") == AWSTemplateExtensionConfig + ) - # Test extension defaults - must provide non-empty config_data or it returns {} - extension_defaults = TemplateExtensionRegistry.get_extension_defaults( - "aws", {"allocation_strategy": "capacity_optimized"} - ) - assert isinstance(extension_defaults, dict) - assert "allocation_strategy" in extension_defaults + # Test extension defaults - must provide non-empty config_data or it returns {} + extension_defaults = TemplateExtensionRegistry.get_extension_defaults( + "aws", {"allocation_strategy": "capacity_optimized"} + ) + assert isinstance(extension_defaults, dict) + assert "allocation_strategy" in extension_defaults + finally: + # Restore the registry to the pre-test state unconditionally. + TemplateExtensionRegistry._extensions.clear() + TemplateExtensionRegistry._extensions.update(saved_extensions) + TemplateExtensionRegistry._extension_instances.clear() + TemplateExtensionRegistry._extension_instances.update(saved_instances) def test_clean_template_schema_validation(self): """Test that cleaned template schema works correctly.""" diff --git a/tests/integration/test_cqrs_migration_baseline.py b/tests/integration/test_cqrs_migration_baseline.py index 179b5b826..cba58fff0 100644 --- a/tests/integration/test_cqrs_migration_baseline.py +++ b/tests/integration/test_cqrs_migration_baseline.py @@ -203,6 +203,17 @@ def create_request_handler( is_final=True, ) ) + from orb.domain.base.results import ProviderSelectionResult + + mock_provider_validation_service = AsyncMock() + mock_provider_validation_service.select_and_validate_provider = AsyncMock( + return_value=ProviderSelectionResult( + provider_type="aws", + provider_name="aws-default", + selection_reason="Best match for template requirements", + confidence=0.95, + ) + ) return CreateMachineRequestHandler( uow_factory=mock_uow_factory, logger=mock_logger, @@ -212,6 +223,7 @@ def create_request_handler( query_bus=mock_query_bus, provider_selection_port=mock_provider_selection_port, provisioning_service=mock_provisioning_service, + provider_validation_service=mock_provider_validation_service, ) @pytest.fixture @@ -240,7 +252,7 @@ async def test_create_request_command_handler(self, create_request_handler): # Verify handler interactions create_request_handler._query_bus.execute.assert_called_once() - create_request_handler._provider_selection_port.select_provider_for_template.assert_called_once() + create_request_handler._provider_validation_service.select_and_validate_provider.assert_called_once() create_request_handler._provisioning_service.execute_provisioning.assert_called_once() @pytest.mark.asyncio diff --git a/tests/integration/test_security_features.py b/tests/integration/test_security_features.py index 6b71470ce..cb29e4172 100644 --- a/tests/integration/test_security_features.py +++ b/tests/integration/test_security_features.py @@ -1,4 +1,4 @@ -"""Integration tests for security features: JWT blacklist, input validation, auth middleware.""" +"""Integration tests for security features: JWT denylist, input validation, auth middleware.""" import time from unittest.mock import AsyncMock, MagicMock @@ -11,7 +11,7 @@ EnhancedBearerTokenStrategy, RateLimiter, ) -from orb.infrastructure.auth.token_blacklist import InMemoryTokenBlacklist, RedisTokenBlacklist +from orb.infrastructure.auth.token_denylist import InMemoryTokenDenylist, RedisTokenDenylist from orb.infrastructure.validation.input_validator import InputValidator, ValidationError # --------------------------------------------------------------------------- @@ -36,15 +36,15 @@ def _make_context( def _make_strategy( - blacklist=None, + denylist=None, rate_limit_enabled: bool = False, token_expiry: int = 3600, ) -> EnhancedBearerTokenStrategy: - if blacklist is None: - blacklist = InMemoryTokenBlacklist() + if denylist is None: + denylist = InMemoryTokenDenylist() return EnhancedBearerTokenStrategy( secret_key=SECRET, - blacklist=blacklist, + denylist=denylist, algorithm="HS256", token_expiry=token_expiry, enabled=True, @@ -53,18 +53,18 @@ def _make_strategy( # =========================================================================== -# JWT Token Blacklist – InMemory +# JWT Token Denylist – InMemory # =========================================================================== -class TestInMemoryBlacklistIntegration: - """Integration tests for InMemoryTokenBlacklist.""" +class TestInMemoryDenylistIntegration: + """Integration tests for InMemoryTokenDenylist.""" @pytest.mark.asyncio async def test_revoke_then_reject(self): - """Token added to blacklist must be rejected by the strategy.""" - blacklist = InMemoryTokenBlacklist() - strategy = _make_strategy(blacklist) + """Token added to denylist must be rejected by the strategy.""" + denylist = InMemoryTokenDenylist() + strategy = _make_strategy(denylist) token = strategy._create_access_token("user1", ["user"], ["read"]) @@ -82,10 +82,10 @@ async def test_revoke_then_reject(self): assert result.status == AuthStatus.INVALID @pytest.mark.asyncio - async def test_blacklist_does_not_affect_other_tokens(self): + async def test_denylist_does_not_affect_other_tokens(self): """Revoking one token must not affect other valid tokens.""" - blacklist = InMemoryTokenBlacklist() - strategy = _make_strategy(blacklist) + denylist = InMemoryTokenDenylist() + strategy = _make_strategy(denylist) token_a = strategy._create_access_token("userA", ["user"], ["read"]) token_b = strategy._create_access_token("userB", ["user"], ["read"]) @@ -99,72 +99,72 @@ async def test_blacklist_does_not_affect_other_tokens(self): assert result_b.is_authenticated @pytest.mark.asyncio - async def test_expired_blacklist_entry_auto_removed(self): - """Blacklist entries with past expiry are treated as not blacklisted.""" - blacklist = InMemoryTokenBlacklist() + async def test_expired_denylist_entry_auto_removed(self): + """Denylist entries with past expiry are treated as not denylisted.""" + denylist = InMemoryTokenDenylist() token = "some.jwt.token" past_expiry = int(time.time()) - 10 # already expired - await blacklist.add_token(token, expires_at=past_expiry) + await denylist.add_token(token, expires_at=past_expiry) - # Should NOT be blacklisted because the entry itself has expired - assert await blacklist.is_blacklisted(token) is False + # Should NOT be denylisted because the entry itself has expired + assert await denylist.is_denylisted(token) is False @pytest.mark.asyncio async def test_cleanup_removes_only_expired(self): """cleanup_expired removes expired entries and keeps valid ones.""" - blacklist = InMemoryTokenBlacklist() + denylist = InMemoryTokenDenylist() - await blacklist.add_token("expired1", int(time.time()) - 5) - await blacklist.add_token("expired2", int(time.time()) - 1) - await blacklist.add_token("valid1", int(time.time()) + 3600) + await denylist.add_token("expired1", int(time.time()) - 5) + await denylist.add_token("expired2", int(time.time()) - 1) + await denylist.add_token("valid1", int(time.time()) + 3600) - removed = await blacklist.cleanup_expired() + removed = await denylist.cleanup_expired() assert removed == 2 - assert await blacklist.is_blacklisted("valid1") is True - assert await blacklist.get_blacklist_size() == 1 + assert await denylist.is_denylisted("valid1") is True + assert await denylist.get_denylist_size() == 1 @pytest.mark.asyncio - async def test_remove_token_from_blacklist(self): - """Explicitly removed token is no longer blacklisted.""" - blacklist = InMemoryTokenBlacklist() + async def test_remove_token_from_denylist(self): + """Explicitly removed token is no longer denylisted.""" + denylist = InMemoryTokenDenylist() token = "removable.token" - await blacklist.add_token(token) - assert await blacklist.is_blacklisted(token) is True + await denylist.add_token(token) + assert await denylist.is_denylisted(token) is True - removed = await blacklist.remove_token(token) + removed = await denylist.remove_token(token) assert removed is True - assert await blacklist.is_blacklisted(token) is False + assert await denylist.is_denylisted(token) is False @pytest.mark.asyncio async def test_remove_nonexistent_token_returns_false(self): """Removing a token that was never added returns False.""" - blacklist = InMemoryTokenBlacklist() - result = await blacklist.remove_token("ghost.token") + denylist = InMemoryTokenDenylist() + result = await denylist.remove_token("ghost.token") assert result is False @pytest.mark.asyncio - async def test_blacklist_size_tracks_additions(self): - """get_blacklist_size reflects the number of active entries.""" - blacklist = InMemoryTokenBlacklist() - assert await blacklist.get_blacklist_size() == 0 + async def test_denylist_size_tracks_additions(self): + """get_denylist_size reflects the number of active entries.""" + denylist = InMemoryTokenDenylist() + assert await denylist.get_denylist_size() == 0 - await blacklist.add_token("t1") - await blacklist.add_token("t2") - await blacklist.add_token("t3") + await denylist.add_token("t1") + await denylist.add_token("t2") + await denylist.add_token("t3") - assert await blacklist.get_blacklist_size() == 3 + assert await denylist.get_denylist_size() == 3 # =========================================================================== -# JWT Token Blacklist – Redis (mocked) +# JWT Token Denylist – Redis (mocked) # =========================================================================== -class TestRedisBlacklistIntegration: - """Integration tests for RedisTokenBlacklist with a mocked Redis client.""" +class TestRedisDenylistIntegration: + """Integration tests for RedisTokenDenylist with a mocked Redis client.""" def _make_redis_mock(self) -> MagicMock: redis = MagicMock() @@ -172,17 +172,17 @@ def _make_redis_mock(self) -> MagicMock: redis.set = AsyncMock(return_value=True) redis.exists = AsyncMock(return_value=1) redis.delete = AsyncMock(return_value=1) - redis.keys = AsyncMock(return_value=["token_blacklist:tok1", "token_blacklist:tok2"]) + redis.keys = AsyncMock(return_value=["token_denylist:tok1", "token_denylist:tok2"]) return redis @pytest.mark.asyncio async def test_add_token_with_expiry_calls_setex(self): """add_token with expiry uses Redis SETEX.""" redis = self._make_redis_mock() - blacklist = RedisTokenBlacklist(redis_client=redis) + denylist = RedisTokenDenylist(redis_client=redis) future_expiry = int(time.time()) + 3600 - result = await blacklist.add_token("mytoken", expires_at=future_expiry) + result = await denylist.add_token("mytoken", expires_at=future_expiry) assert result is True redis.setex.assert_called_once() @@ -191,56 +191,56 @@ async def test_add_token_with_expiry_calls_setex(self): async def test_add_token_without_expiry_calls_set(self): """add_token without expiry uses Redis SET.""" redis = self._make_redis_mock() - blacklist = RedisTokenBlacklist(redis_client=redis) + denylist = RedisTokenDenylist(redis_client=redis) - result = await blacklist.add_token("mytoken") + result = await denylist.add_token("mytoken") assert result is True redis.set.assert_called_once() @pytest.mark.asyncio - async def test_is_blacklisted_returns_true_when_key_exists(self): - """is_blacklisted returns True when Redis key exists.""" + async def test_is_denylisted_returns_true_when_key_exists(self): + """is_denylisted returns True when Redis key exists.""" redis = self._make_redis_mock() redis.exists = AsyncMock(return_value=1) - blacklist = RedisTokenBlacklist(redis_client=redis) + denylist = RedisTokenDenylist(redis_client=redis) - assert await blacklist.is_blacklisted("mytoken") is True + assert await denylist.is_denylisted("mytoken") is True @pytest.mark.asyncio - async def test_is_blacklisted_returns_false_when_key_missing(self): - """is_blacklisted returns False when Redis key does not exist.""" + async def test_is_denylisted_returns_false_when_key_missing(self): + """is_denylisted returns False when Redis key does not exist.""" redis = self._make_redis_mock() redis.exists = AsyncMock(return_value=0) - blacklist = RedisTokenBlacklist(redis_client=redis) + denylist = RedisTokenDenylist(redis_client=redis) - assert await blacklist.is_blacklisted("mytoken") is False + assert await denylist.is_denylisted("mytoken") is False @pytest.mark.asyncio async def test_redis_error_fails_secure(self): - """On Redis error, is_blacklisted fails secure (returns True).""" + """On Redis error, is_denylisted fails secure (returns True).""" redis = self._make_redis_mock() redis.exists = AsyncMock(side_effect=ConnectionError("Redis down")) - blacklist = RedisTokenBlacklist(redis_client=redis) + denylist = RedisTokenDenylist(redis_client=redis) - # Fail-secure: assume blacklisted on error - assert await blacklist.is_blacklisted("anytoken") is True + # Fail-secure: assume denylisted on error + assert await denylist.is_denylisted("anytoken") is True @pytest.mark.asyncio async def test_fallback_to_in_memory_when_no_redis(self): - """Without a Redis client, RedisTokenBlacklist falls back to in-memory.""" - blacklist = RedisTokenBlacklist(redis_client=None) + """Without a Redis client, RedisTokenDenylist falls back to in-memory.""" + denylist = RedisTokenDenylist(redis_client=None) - await blacklist.add_token("fallback_token") - assert await blacklist.is_blacklisted("fallback_token") is True + await denylist.add_token("fallback_token") + assert await denylist.is_denylisted("fallback_token") is True @pytest.mark.asyncio - async def test_get_blacklist_size_counts_keys(self): - """get_blacklist_size counts matching Redis keys.""" + async def test_get_denylist_size_counts_keys(self): + """get_denylist_size counts matching Redis keys.""" redis = self._make_redis_mock() - blacklist = RedisTokenBlacklist(redis_client=redis) + denylist = RedisTokenDenylist(redis_client=redis) - size = await blacklist.get_blacklist_size() + size = await denylist.get_denylist_size() assert size == 2 # matches the two keys in the mock @@ -300,10 +300,10 @@ async def test_non_bearer_scheme_fails(self): assert not result.is_authenticated @pytest.mark.asyncio - async def test_blacklisted_token_rejected_via_authenticate(self): + async def test_denylisted_token_rejected_via_authenticate(self): """authenticate rejects a token that has been revoked.""" - blacklist = InMemoryTokenBlacklist() - strategy = _make_strategy(blacklist) + denylist = InMemoryTokenDenylist() + strategy = _make_strategy(denylist) token = strategy._create_access_token("bob", ["user"], ["read"]) await strategy.revoke_token(token) @@ -393,11 +393,11 @@ async def test_access_token_cannot_be_used_as_refresh_token(self): @pytest.mark.asyncio async def test_revoked_refresh_token_rejected(self): """A revoked refresh token cannot be used to get a new access token.""" - blacklist = InMemoryTokenBlacklist() - strategy = _make_strategy(blacklist) + denylist = InMemoryTokenDenylist() + strategy = _make_strategy(denylist) refresh = strategy.create_refresh_token("frank", ["user"], ["read"]) - await blacklist.add_token(refresh) + await denylist.add_token(refresh) result = await strategy.refresh_token(refresh) @@ -454,10 +454,10 @@ def test_different_ips_tracked_independently(self): @pytest.mark.asyncio async def test_rate_limited_ip_gets_failed_auth_result(self): """Strategy returns FAILED when IP is rate-limited.""" - blacklist = InMemoryTokenBlacklist() + denylist = InMemoryTokenDenylist() strategy = EnhancedBearerTokenStrategy( secret_key=SECRET, - blacklist=blacklist, + denylist=denylist, algorithm="HS256", enabled=True, rate_limit_enabled=True, diff --git a/tests/onaws/__init__.py b/tests/onaws/__init__.py deleted file mode 100644 index 1b7b680fc..000000000 --- a/tests/onaws/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# AWS-specific tests package diff --git a/tests/onmoto/__init__.py b/tests/providers/aws/__init__.py similarity index 100% rename from tests/onmoto/__init__.py rename to tests/providers/aws/__init__.py diff --git a/tests/onmoto/conftest.py b/tests/providers/aws/conftest.py similarity index 88% rename from tests/onmoto/conftest.py rename to tests/providers/aws/conftest.py index 302fadb54..f83dfeed0 100644 --- a/tests/onmoto/conftest.py +++ b/tests/providers/aws/conftest.py @@ -1,4 +1,20 @@ -"""Shared fixtures for moto-based full-pipeline integration tests.""" +"""Provider-level fixtures for the AWS test tree. + +Covers both moto (mocked) and live subtrees. The fixtures that call real +AWS are gated on the --live flag so they are never accidentally executed in +CI. + +This file is the canonical source of: + - moto_aws context manager + - moto_vpc_resources + - orb_config_dir (moto-backed) + - Live-test session fixtures (gated by --live) + +Individual conftest.py files under moto/ and live/ may import helpers +defined here or in their own modules but should not duplicate them. +""" + +from __future__ import annotations import json import os @@ -12,7 +28,7 @@ import pytest from moto import mock_aws -sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src")) from orb.providers.aws.domain.template.aws_template_aggregate import AWSTemplate from orb.providers.aws.infrastructure.aws_client import AWSClient @@ -24,26 +40,22 @@ from orb.providers.aws.utilities.aws_operations import AWSOperations from tests.utilities.reset_singletons import reset_all_singletons -_PROJECT_ROOT = Path(__file__).parent.parent.parent +_PROJECT_ROOT = Path(__file__).parent.parent.parent.parent _CONFIG_SOURCE = _PROJECT_ROOT / "config" REGION = "eu-west-2" # --------------------------------------------------------------------------- -# Moto compatibility patches +# Moto compatibility patches (applied to all moto/ tests via autouse=True +# in tests/providers/aws/moto/conftest.py) # --------------------------------------------------------------------------- -@pytest.fixture(autouse=True) -def patch_moto_compat(): - """Patch moto-incompatible behaviours for all onmoto tests. - - 1. AWSImageResolutionService.is_resolution_needed -> False - Prevents SSM path resolution which moto cannot fulfil. +def make_patch_moto_compat(): + """Return a context manager that patches moto-incompatible behaviours. - 2. AWSProvisioningAdapter._provision_via_handlers synthesises instances - from instance_ids so the orchestration loop sees fulfilled_count > 0. + Extracted as a helper so it can be reused from the moto/ conftest. """ from orb.providers.aws.infrastructure.adapters.aws_provisioning_adapter import ( AWSProvisioningAdapter, @@ -57,9 +69,6 @@ def _patched_provision(self, request, template, dry_run=False): instance_ids = result.get("instance_ids") or result.get("resource_ids", []) iids = [i for i in instance_ids if i.startswith("i-")] if iids: - # Resolve the resource_id (reservation ID) for each instance so that - # group_by_resource can group them correctly during return/deprovisioning. - # resource_ids contains the reservation ID(s); use the first one. resource_ids = result.get("resource_ids", []) resource_id = next( (r for r in resource_ids if not r.startswith("i-")), @@ -81,15 +90,21 @@ def _patched_provision(self, request, template, dry_run=False): ] return result - with ( - patch( - "orb.providers.aws.infrastructure.services.aws_image_resolution_service" - ".AWSImageResolutionService.is_resolution_needed", - return_value=False, - ), - patch.object(AWSProvisioningAdapter, "_provision_via_handlers", _patched_provision), - ): - yield + import contextlib + + @contextlib.contextmanager + def _ctx(): + with ( + patch( + "orb.providers.aws.infrastructure.services.aws_image_resolution_service" + ".AWSImageResolutionService.is_resolution_needed", + return_value=False, + ), + patch.object(AWSProvisioningAdapter, "_provision_via_handlers", _patched_provision), + ): + yield + + return _ctx() # --------------------------------------------------------------------------- @@ -139,24 +154,19 @@ def moto_vpc_resources(moto_aws): # --------------------------------------------------------------------------- -# ORB config directory +# ORB config directory (moto-backed) # --------------------------------------------------------------------------- @pytest.fixture def orb_config_dir(tmp_path, moto_vpc_resources): - """Generate a complete ORB config directory pointing at moto VPC resources. - - Writes config.json, aws_templates.json, and default_config.json into - tmp_path/config/, sets ORB_CONFIG_DIR, and returns the config dir path. - """ + """Generate a complete ORB config directory pointing at moto VPC resources.""" config_dir = tmp_path / "config" config_dir.mkdir(parents=True, exist_ok=True) subnet_ids = moto_vpc_resources["subnet_ids"] sg_id = moto_vpc_resources["sg_id"] - # --- config.json --- config_data = { "scheduler": { "type": "hostfactory", @@ -200,14 +210,11 @@ def orb_config_dir(tmp_path, moto_vpc_resources): with open(config_dir / "config.json", "w") as f: json.dump(config_data, f, indent=2) - # --- aws_templates.json --- - # Load via the real scheduler pipeline (HF camelCase source → HF wire format) try: - from tests.onaws.template_processor import TemplateProcessor + from tests.providers.aws.live.template_processor import TemplateProcessor templates_data = TemplateProcessor.generate_templates_programmatically("hostfactory") except Exception: - # Fallback: copy the source file directly if programmatic generation fails src = _CONFIG_SOURCE / "aws_templates.json" if src.exists(): shutil.copy2(src, config_dir / "aws_templates.json") @@ -217,17 +224,14 @@ def orb_config_dir(tmp_path, moto_vpc_resources): with open(config_dir / "aws_templates.json", "w") as f: json.dump(templates_data, f, indent=2) - # --- default_config.json --- default_src = _CONFIG_SOURCE / "default_config.json" if default_src.exists(): shutil.copy2(default_src, config_dir / "default_config.json") - # Point ORB at this config directory os.environ["ORB_CONFIG_DIR"] = str(config_dir) yield config_dir - # Cleanup env var after test os.environ.pop("ORB_CONFIG_DIR", None) @@ -237,7 +241,7 @@ def orb_config_dir(tmp_path, moto_vpc_resources): @pytest.fixture(autouse=True) -def reset_singletons(): +def reset_singletons_aws(): """Reset DI container and all singletons before and after each test.""" from orb.infrastructure.di.container import reset_container @@ -249,7 +253,7 @@ def reset_singletons(): # --------------------------------------------------------------------------- -# AWS clients +# AWS clients (moto-backed) # --------------------------------------------------------------------------- @@ -266,7 +270,7 @@ def autoscaling_client(moto_aws): # --------------------------------------------------------------------------- -# Handler factory helpers (shared with handler-level tests) +# Handler factory helpers (shared across moto/ and contract/ subtrees) # --------------------------------------------------------------------------- @@ -289,9 +293,6 @@ def _make_config_port(prefix: str = ""): provider_config = MagicMock() provider_config.provider_defaults = {"aws": provider_defaults} config_port.get_provider_config.return_value = provider_config - # Set app_config to None so that accessing .circuit_breaker on it raises - # AttributeError, causing _get_circuit_breaker_config to use integer defaults - # instead of a MagicMock (which breaks int comparisons in the circuit breaker) config_port.app_config = None return config_port @@ -386,36 +387,37 @@ def _inject_moto_factory(aws_client: AWSClient, logger, config_port) -> None: return lt_manager = _make_launch_template_manager(aws_client, logger) - aws_ops = AWSOperations(aws_client, logger, cfg_port) - factory = AWSHandlerFactory(aws_client=aws_client, logger=logger, config=cfg_port) + effective_config = cfg_port if config_port is None else config_port + aws_ops = AWSOperations(aws_client, logger, effective_config) + factory = AWSHandlerFactory(aws_client=aws_client, logger=logger, config=effective_config) factory._handlers[ProviderApi.ASG.value] = ASGHandler( aws_client=aws_client, logger=logger, aws_ops=aws_ops, launch_template_manager=lt_manager, - config_port=cfg_port, + config_port=effective_config, ) factory._handlers[ProviderApi.EC2_FLEET.value] = EC2FleetHandler( aws_client=aws_client, logger=logger, aws_ops=aws_ops, launch_template_manager=lt_manager, - config_port=cfg_port, + config_port=effective_config, ) factory._handlers[ProviderApi.RUN_INSTANCES.value] = RunInstancesHandler( aws_client=aws_client, logger=logger, aws_ops=aws_ops, launch_template_manager=lt_manager, - config_port=cfg_port, + config_port=effective_config, ) factory._handlers[ProviderApi.SPOT_FLEET.value] = SpotFleetHandler( aws_client=aws_client, logger=logger, aws_ops=aws_ops, launch_template_manager=lt_manager, - config_port=cfg_port, + config_port=effective_config, ) strategy._aws_client = aws_client diff --git a/tests/unit/providers/aws/__init__.py b/tests/providers/aws/live/__init__.py similarity index 100% rename from tests/unit/providers/aws/__init__.py rename to tests/providers/aws/live/__init__.py diff --git a/tests/onaws/cleanup_helpers.py b/tests/providers/aws/live/cleanup_helpers.py similarity index 100% rename from tests/onaws/cleanup_helpers.py rename to tests/providers/aws/live/cleanup_helpers.py diff --git a/tests/onaws/conftest.py b/tests/providers/aws/live/conftest.py similarity index 54% rename from tests/onaws/conftest.py rename to tests/providers/aws/live/conftest.py index 19e3d6169..b09d6d4ac 100644 --- a/tests/onaws/conftest.py +++ b/tests/providers/aws/live/conftest.py @@ -1,4 +1,9 @@ -"""onaws integration test configuration.""" +"""Live AWS integration test configuration. + +Tests in this subtree require real AWS credentials and a pre-configured ORB +environment (``orb init``). They are skipped by default; pass ``--live`` (or +the legacy ``--run-aws``) to enable them. +""" import json import logging @@ -12,8 +17,22 @@ from boto3 import Session from botocore.exceptions import ClientError, NoCredentialsError + +def _find_repo_root(start: Path) -> Path: + """Walk up from *start* until we find pyproject.toml. Hard-fail otherwise. + + Robust against test-tree reshuffles: counting parents (e.g. `parent ** 5`) + silently breaks the moment a test file moves up or down the tree. The + pyproject.toml marker is stable as long as the repo has one. + """ + for candidate in (start, *start.parents): + if (candidate / "pyproject.toml").is_file(): + return candidate + raise RuntimeError(f"Could not locate repo root (no pyproject.toml) above {start}") + + # Ensure repo root is on sys.path so hfmock.py and other root-level modules are importable -repo_root = Path(__file__).parent.parent.parent +repo_root = _find_repo_root(Path(__file__).resolve().parent) if str(repo_root) not in sys.path: sys.path.insert(0, str(repo_root)) @@ -23,49 +42,50 @@ logs_dir.mkdir(exist_ok=True) +def _is_live_run(config) -> bool: + """Return True when live tests have been explicitly requested.""" + return config.getoption("--live", default=False) or config.getoption("--run-aws", default=False) + + def _get_aws_profile_and_region() -> tuple[str | None, str | None]: """Read profile and region from ORB config. - Priority: - 1. ORB_CONFIG_DIR env var (per-test config dir) - 2. Project config/config.json (written by orb init) - 3. AWS_REGION / AWS_DEFAULT_REGION env vars + Delegates to ORB's own config-discovery (``orb.config.platform_dirs``) so + test discovery never drifts from runtime discovery. Falls back to the + AWS_REGION / AWS_DEFAULT_REGION env vars for the region only. """ - candidates = [] - config_dir = os.environ.get("ORB_CONFIG_DIR") - if config_dir: - candidates.append(os.path.join(config_dir, "config.json")) - # Fall back to the project's real config written by orb init - candidates.append(str(repo_root / "config" / "config.json")) - - for config_path in candidates: - try: - with open(config_path) as f: - config = json.load(f) - providers = config.get("provider", {}).get("providers", []) - if providers: - provider_config = providers[0].get("config", {}) - profile = provider_config.get("profile") - region = provider_config.get("region") - if profile or region: - return profile, region - except Exception: - pass + from orb.config.platform_dirs import get_config_location + + config_path = get_config_location() / "config.json" + try: + with open(config_path) as f: + config = json.load(f) + providers = config.get("provider", {}).get("providers", []) + if providers: + provider_config = providers[0].get("config", {}) + profile = provider_config.get("profile") + region = provider_config.get("region") + if profile or region: + return profile, region + except FileNotFoundError: + pass region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") return None, region def pytest_configure(config) -> None: - """Pre-flight check: verify orb init has been run.""" - config_dir = os.environ.get("ORB_CONFIG_DIR", ".") - config_path = Path(config_dir) + """Pre-flight check: verify orb init has been run (only when --live is active).""" + if not _is_live_run(config): + return + + from orb.config.platform_dirs import get_scripts_location - scripts_dir = config_path / "scripts" + scripts_dir = get_scripts_location() if not scripts_dir.exists(): pytest.exit( - "onaws pre-flight failed: scripts/ directory not found.\n" + "live pre-flight failed: scripts/ directory not found.\n" "Run 'orb init' first to set up the environment.\n" f"Looked in: {scripts_dir.resolve()}", returncode=1, @@ -74,7 +94,7 @@ def pytest_configure(config) -> None: invoke_script = scripts_dir / "invoke_provider.sh" if not invoke_script.exists(): pytest.exit( - "onaws pre-flight failed: scripts/invoke_provider.sh not found.\n" + "live pre-flight failed: scripts/invoke_provider.sh not found.\n" "Run 'orb init' first to set up the environment.\n" f"Looked in: {invoke_script.resolve()}", returncode=1, @@ -84,19 +104,33 @@ def pytest_configure(config) -> None: def pytest_sessionstart(session: pytest.Session) -> None: """Check AWS credentials once before any AWS tests run. - Only runs when --run-aws is passed. Calls sts:GetCallerIdentity and exits - immediately if credentials are invalid so no tests are attempted. + Only runs when --live (or --run-aws) is passed. Calls sts:GetCallerIdentity + and exits immediately if credentials are invalid so no tests are attempted. + + Also exports AWS_PROFILE and AWS_REGION/AWS_DEFAULT_REGION from the ORB + config into the process env so that bare ``boto3.Session()`` instances + inside test modules and ORB SDK code pick the right account+region. Any + pre-existing AWS_PROFILE / AWS_REGION env vars are overridden — the ORB + config is the single source of truth for live test runs. """ - if not session.config.getoption("--run-aws", default=False): + if not _is_live_run(session.config): return profile, region = _get_aws_profile_and_region() region = region or "eu-west-1" + + # Override caller's shell env so bare boto3.Session() honours config + if profile: + os.environ["AWS_PROFILE"] = profile + os.environ["AWS_REGION"] = region + os.environ["AWS_DEFAULT_REGION"] = region + try: boto_session = Session(profile_name=profile, region_name=region) sts = boto_session.client("sts", region_name=region) identity = sts.get_caller_identity() print( - f"\n✓ AWS credentials valid: {identity.get('Arn')} (account: {identity.get('Account')})" + f"\n✓ AWS credentials valid: {identity.get('Arn')} " + f"(account: {identity.get('Account')}, region: {region}, profile: {profile!r})" ) except NoCredentialsError as e: pytest.exit(f"AWS credentials not found (profile={profile!r}): {e}", returncode=1) @@ -140,7 +174,7 @@ def nuclear_cleanup(test_session_id: str): yield try: - from tests.onaws.cleanup_helpers import cleanup_all_orb_resources + from tests.providers.aws.live.cleanup_helpers import cleanup_all_orb_resources profile, region = _get_aws_profile_and_region() region = region or "eu-west-1" diff --git a/tests/onaws/parse_output.py b/tests/providers/aws/live/parse_output.py similarity index 100% rename from tests/onaws/parse_output.py rename to tests/providers/aws/live/parse_output.py diff --git a/tests/onaws/plugin_io_schemas.py b/tests/providers/aws/live/plugin_io_schemas.py similarity index 99% rename from tests/onaws/plugin_io_schemas.py rename to tests/providers/aws/live/plugin_io_schemas.py index 07e430ce8..729fad4d2 100644 --- a/tests/onaws/plugin_io_schemas.py +++ b/tests/providers/aws/live/plugin_io_schemas.py @@ -103,7 +103,6 @@ "name", "result", "status", - "privateIpAddress", "launchtime", "message", ], @@ -221,7 +220,6 @@ "name", "result", "status", - "private_ip_address", "launch_time", "message", ], diff --git a/tests/onaws/resource-history-capture.md b/tests/providers/aws/live/resource-history-capture.md similarity index 100% rename from tests/onaws/resource-history-capture.md rename to tests/providers/aws/live/resource-history-capture.md diff --git a/tests/onaws/scenarios.py b/tests/providers/aws/live/scenarios.py similarity index 87% rename from tests/onaws/scenarios.py rename to tests/providers/aws/live/scenarios.py index cd34769ae..57908695a 100644 --- a/tests/onaws/scenarios.py +++ b/tests/providers/aws/live/scenarios.py @@ -1,9 +1,12 @@ import itertools import json +import logging import os from pathlib import Path from typing import Any, Dict, List +logger = logging.getLogger(__name__) + def _get_templates_for_resolution() -> List[Dict[str, Any]]: """Get templates for ID resolution using handler classmethods, with filesystem fallback.""" @@ -368,7 +371,7 @@ def generate_scenarios_from_attributes( # Generate all combinations for combination in itertools.product(*attribute_values): # Create the overrides dictionary from the combination - overrides = dict(zip(attribute_names, combination)) + overrides = dict(zip(attribute_names, combination, strict=False)) # Generate test name if naming_template: @@ -548,3 +551,74 @@ def get_test_cases() -> List[Dict[str, Any]]: all_scenarios.extend(CUSTOM_TEST_CASES) return all_scenarios + + +def _load_template_vm_types(template_id: str) -> Dict[str, Any]: + """Look up vmTypes for *template_id* from aws_templates.json. + + Returns an empty dict when the file is absent or the template is not found. + """ + templates_path = ( + Path(__file__).parent.parent.parent.parent.parent / "config" / "aws_templates.json" + ) + if not templates_path.exists(): + return {} + try: + with open(templates_path) as f: + data = json.load(f) + for tpl in data.get("templates", []): + if tpl.get("templateId") == template_id: + return tpl.get("vmTypes") or {} + except (FileNotFoundError, json.JSONDecodeError): + return {} + except Exception: + logger.warning( + "_load_template_vm_types: unexpected error reading %s for template %r", + templates_path, + template_id, + exc_info=True, + ) + raise + return {} + + +# Provider APIs that do not use capacity-unit weighting even when their template +# config lists weights. RunInstances always provisions one instance per unit. +_UNWEIGHTED_PROVIDER_APIS = {"RunInstances"} + + +def template_uses_weighted_capacity(test_case: Dict[str, Any]) -> bool: + """Return True if this scenario's effective vmTypes use weights > 1. + + Weighted templates fulfil capacity in *capacity units*, not instance count. + AWS EC2Fleet / SpotFleet / ASG all support mixed-instance weighting: when a + template lists vmTypes with weights > 1, requested_count is in capacity units + and the actual instance count depends on which weights AWS picks. + + Resolution order for vmTypes: + 1. ``overrides["vmTypes"]`` — explicit per-scenario override takes precedence. + 2. The template's own vmTypes read from ``config/aws_templates.json``. + + RunInstances is always unweighted: the API provisions exactly one instance per + requested unit regardless of any weight values in the template config. + """ + overrides = test_case.get("overrides", {}) or {} + + # RunInstances never uses capacity-unit weighting. + provider_api = overrides.get("providerApi") or "" + if provider_api in _UNWEIGHTED_PROVIDER_APIS: + return False + + # 1. Use explicit override if present. + vm_types: Dict[str, Any] | None = overrides.get("vmTypes") or overrides.get("vm_types") + + # 2. Fall back to the template's own vmTypes. + if not vm_types: + template_id = test_case.get("template_id") or "" + if template_id: + vm_types = _load_template_vm_types(template_id) or None + + if not vm_types: + return False + + return any(int(w) > 1 for w in vm_types.values()) diff --git a/tests/onaws/scenarios_mcp.py b/tests/providers/aws/live/scenarios_mcp.py similarity index 100% rename from tests/onaws/scenarios_mcp.py rename to tests/providers/aws/live/scenarios_mcp.py diff --git a/tests/onaws/scenarios_rest_api.py b/tests/providers/aws/live/scenarios_rest_api.py similarity index 98% rename from tests/onaws/scenarios_rest_api.py rename to tests/providers/aws/live/scenarios_rest_api.py index 2460a5426..095da43d8 100644 --- a/tests/onaws/scenarios_rest_api.py +++ b/tests/providers/aws/live/scenarios_rest_api.py @@ -3,7 +3,7 @@ import itertools from typing import Any, Dict, List, Optional -from tests.onaws.scenarios import ( +from tests.providers.aws.live.scenarios import ( CUSTOM_TEST_CASES as CUSTOM_TEST_CASES, DEFAULT_ATTRIBUTE_COMBINATIONS as _DEFAULT_ATTRIBUTE_COMBINATIONS, resolve_template_id, @@ -56,7 +56,7 @@ # Centralized timeouts/constants for REST API tests REST_API_TIMEOUTS = { - "rest_api_timeout": 60, # Per-request HTTP timeout for REST calls + "rest_api_timeout": 120, # Per-request HTTP timeout for REST calls (large ABIS scenarios may exceed 60s) "rest_api_retry_attempts": 3, # Retry attempts for REST calls (when implemented) "server_start": 30, # How long to wait for the server to start responding to health "health_check": 5, # Health endpoint timeout diff --git a/tests/onaws/scenarios_sdk.py b/tests/providers/aws/live/scenarios_sdk.py similarity index 100% rename from tests/onaws/scenarios_sdk.py rename to tests/providers/aws/live/scenarios_sdk.py diff --git a/tests/onaws/template_processor.py b/tests/providers/aws/live/template_processor.py similarity index 95% rename from tests/onaws/template_processor.py rename to tests/providers/aws/live/template_processor.py index 0c09a89e9..73a86a282 100644 --- a/tests/onaws/template_processor.py +++ b/tests/providers/aws/live/template_processor.py @@ -9,8 +9,8 @@ config/aws_templates.json ──load_templates_from_path──► raw dicts (with real image_id) ──format_templates_for_generation──► scheduler wire format ──copy + override──► run_templates//aws_templates.json - config/config.json ──merge overrides──► run_templates//config.json - config/default_config.json ──copy──────────────► run_templates//default_config.json + config/config.json ──merge overrides──► run_templates//config/config.json + config/default_config.json ──copy──────────────► run_templates//config/default_config.json """ import json @@ -91,18 +91,26 @@ def _normalize_key(key: str, target_fmt: str) -> str: return _SNAKE_TO_CAMEL.get(key, key) +def _find_repo_root(start: Path) -> Path: + """Walk up from *start* until pyproject.toml is found. Hard-fail otherwise.""" + for candidate in (start, *start.parents): + if (candidate / "pyproject.toml").is_file(): + return candidate + raise RuntimeError(f"Could not locate repo root (no pyproject.toml) above {start}") + + class TemplateProcessor: """Generates per-test config directories from the real project config files.""" def __init__(self, base_dir: str | None = None): resolved: Path if base_dir is None: - resolved = Path(__file__).parent + resolved = Path(__file__).resolve().parent else: resolved = Path(base_dir) self.base_dir = resolved - self.config_source_dir = resolved.parent.parent / "config" + self.config_source_dir = _find_repo_root(resolved) / "config" self.run_templates_dir = resolved / "run_templates" # ------------------------------------------------------------------ @@ -257,10 +265,11 @@ def generate_templates_programmatically(scheduler_type: str = "hostfactory") -> """ import sys - sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) + repo_root = _find_repo_root(Path(__file__).resolve().parent) + sys.path.insert(0, str(repo_root / "src")) # Find the real generated templates file in config/ - config_dir = Path(__file__).parent.parent.parent / "config" + config_dir = repo_root / "config" templates_file = config_dir / "aws_templates.json" if not templates_file.exists(): raise FileNotFoundError( diff --git a/tests/onaws/test_cleanup_e2e_onaws.py b/tests/providers/aws/live/test_cleanup_e2e_onaws.py similarity index 71% rename from tests/onaws/test_cleanup_e2e_onaws.py rename to tests/providers/aws/live/test_cleanup_e2e_onaws.py index f859f84fa..6c68a6074 100644 --- a/tests/onaws/test_cleanup_e2e_onaws.py +++ b/tests/providers/aws/live/test_cleanup_e2e_onaws.py @@ -13,23 +13,24 @@ import boto3.session import pytest +from botocore.exceptions import ClientError -from tests.onaws import scenarios -from tests.onaws.cleanup_helpers import ( +from tests.providers.aws.live import scenarios +from tests.providers.aws.live.cleanup_helpers import ( cleanup_launch_templates_for_request, get_machine_ids_from_ec2 as _get_machine_ids_from_ec2_helper, wait_for_instances_terminated, ) -from tests.onaws.scenarios import SPOT_VM_TYPES -from tests.onaws.template_processor import TemplateProcessor +from tests.providers.aws.live.scenarios import SPOT_VM_TYPES +from tests.providers.aws.live.template_processor import TemplateProcessor try: - from tests.onaws.scenarios_sdk import SDK_TIMEOUTS # type: ignore[import] + from tests.providers.aws.live.scenarios_sdk import SDK_TIMEOUTS # type: ignore[import] except ImportError: SDK_TIMEOUTS = {"request_completion": 600, "return_completion": 300, "poll_interval": 5} try: - from tests.onaws.test_onaws import ( + from tests.providers.aws.live.test_onaws import ( _check_all_ec2_hosts_are_being_terminated, _get_resource_id_from_instance, get_instance_state, @@ -52,15 +53,71 @@ os.environ["LOG_DESTINATION"] = "file" os.environ.setdefault("AWS_REGION", "eu-west-1") -_boto_session = boto3.session.Session() -_ec2_region = ( - os.environ.get("AWS_REGION") - or os.environ.get("AWS_DEFAULT_REGION") - or _boto_session.region_name - or "eu-west-1" -) -ec2_client = _boto_session.client("ec2", region_name=_ec2_region) -asg_client = _boto_session.client("autoscaling", region_name=_ec2_region) +_ec2_client = None +_asg_client = None + + +def _get_boto_profile_and_region() -> tuple[str | None, str]: + """Read AWS profile and region from ORB_CONFIG_DIR config. + + Falls back to AWS_PROFILE env var so that an explicit profile is always + passed to boto3. This is necessary because pytest-env injects fake env + var credentials (AWS_ACCESS_KEY_ID=testing) for unit tests; botocore uses + env var credentials before profile credentials, so any session created + without an explicit profile_name ends up using the fake credentials and + gets AuthFailure from real AWS calls. Passing an explicit profile_name + forces botocore into profile-based credential resolution, bypassing the + injected env vars. + """ + import json as _json + + profile = None + region = None + config_dir = os.environ.get("ORB_CONFIG_DIR") + if config_dir: + try: + config_path = os.path.join(config_dir, "config.json") + with open(config_path) as _f: + config = _json.load(_f) + providers = config.get("provider", {}).get("providers", []) + if providers: + provider_cfg = providers[0].get("config", {}) + profile = provider_cfg.get("profile") + region = provider_cfg.get("region") + except Exception: + pass + # Fall back to AWS_PROFILE env var so the explicit profile_name is always + # non-None when a profile is available. This forces botocore to use + # profile-based credentials and ignore injected fake env var credentials. + profile = profile or os.environ.get("AWS_PROFILE") + region = ( + region + or os.environ.get("AWS_REGION") + or os.environ.get("AWS_DEFAULT_REGION") + or "eu-west-1" + ) + return profile, region + + +def _get_ec2_client(): + global _ec2_client + if _ec2_client is None: + _profile, _region = _get_boto_profile_and_region() + _ec2_client = boto3.session.Session(profile_name=_profile, region_name=_region).client( + "ec2", region_name=_region + ) + return _ec2_client + + +def _get_asg_client(): + global _asg_client + if _asg_client is None: + _profile, _region = _get_boto_profile_and_region() + _asg_client = boto3.session.Session(profile_name=_profile, region_name=_region).client( + "autoscaling", region_name=_region + ) + return _asg_client + log = logging.getLogger("cleanup_e2e_test") log.setLevel(logging.DEBUG) @@ -159,13 +216,34 @@ # --------------------------------------------------------------------------- -def _assert_asg_deleted(asg_name: str, timeout: int = 120) -> None: - """Assert ASG is deleted or has zero desired capacity with no instances.""" +def _assert_asg_deleted(asg_name: str, timeout: int = 300, weighted: bool = False) -> None: + """Assert ASG is deleted or has zero desired capacity with no active instances. + + Args: + asg_name: Name of the Auto Scaling Group to check. + timeout: Maximum seconds to wait. + weighted: When True (weighted-capacity template), the lenient condition is used: + no active instances is sufficient even if DesiredCapacity > 0, because + ``ShouldDecrementDesiredCapacity=False`` decrements by instance count rather + than by capacity weight, so the counter may not reach zero. + When False (unweighted template), the strict condition is required: + DesiredCapacity must be 0 AND there must be no active instances. + """ deadline = time.time() + timeout last_state = None + _terminal_lifecycle = frozenset( + { + "Terminating", + "Terminating:Wait", + "Terminating:Proceed", + "Terminated", + "Detaching", + "Detached", + } + ) while time.time() < deadline: try: - resp = asg_client.describe_auto_scaling_groups(AutoScalingGroupNames=[asg_name]) + resp = _get_asg_client().describe_auto_scaling_groups(AutoScalingGroupNames=[asg_name]) groups = resp.get("AutoScalingGroups", []) if not groups: log.info("ASG %s: not found (deleted)", asg_name) @@ -173,25 +251,42 @@ def _assert_asg_deleted(asg_name: str, timeout: int = 120) -> None: g = groups[0] desired = g.get("DesiredCapacity", -1) instances = g.get("Instances", []) - last_state = f"DesiredCapacity={desired}, Instances={len(instances)}" - if desired == 0 and not instances: - log.info("ASG %s: DesiredCapacity=0 with no instances", asg_name) + active_instances = [ + i for i in instances if i.get("LifecycleState", "") not in _terminal_lifecycle + ] + last_state = ( + f"DesiredCapacity={desired}, Instances={len(instances)}, " + f"ActiveInstances={len(active_instances)}" + ) + if weighted: + # Lenient check for weighted templates: no active instances is sufficient. + # ShouldDecrementDesiredCapacity decrements by instance count, not weight, + # so DesiredCapacity may remain > 0 after all instances are gone. + passed = not active_instances + else: + # Strict check for unweighted templates: DesiredCapacity must reach 0. + passed = desired == 0 and not active_instances + if passed: + log.info("ASG %s: no active instances (DesiredCapacity=%s)", asg_name, desired) return log.debug("ASG %s: %s — waiting", asg_name, last_state) except Exception as exc: + # Surface the exception type so it appears verbatim in test output + # instead of the opaque "Last state: None" message. + last_state = f"describe_error={type(exc).__name__}: {exc}" log.warning("_assert_asg_deleted: describe failed for %s: %s", asg_name, exc) time.sleep(10) pytest.fail(f"ASG {asg_name} not deleted or zeroed within {timeout}s. Last state: {last_state}") -def _assert_fleet_deleted(fleet_id: str, timeout: int = 120) -> None: +def _assert_fleet_deleted(fleet_id: str, timeout: int = 300) -> None: """Assert EC2 Fleet is deleted or has zero total target capacity.""" deadline = time.time() + timeout last_state = None - deleted_states = {"deleted", "deleted-running", "deleted-terminating"} + deleted_states = {"deleted", "deleted_running", "deleted_terminating"} while time.time() < deadline: try: - resp = ec2_client.describe_fleets(FleetIds=[fleet_id]) + resp = _get_ec2_client().describe_fleets(FleetIds=[fleet_id]) fleets = resp.get("Fleets", []) if not fleets: log.info("EC2 Fleet %s: not found (deleted)", fleet_id) @@ -208,6 +303,7 @@ def _assert_fleet_deleted(fleet_id: str, timeout: int = 120) -> None: if "InvalidFleetId" in str(exc) or "NotFound" in str(exc): log.info("EC2 Fleet %s: not found (deleted)", fleet_id) return + last_state = f"describe_error={type(exc).__name__}: {exc}" log.warning("_assert_fleet_deleted: describe failed for %s: %s", fleet_id, exc) time.sleep(10) pytest.fail( @@ -215,14 +311,14 @@ def _assert_fleet_deleted(fleet_id: str, timeout: int = 120) -> None: ) -def _assert_spot_fleet_deleted(sfr_id: str, timeout: int = 120) -> None: +def _assert_spot_fleet_deleted(sfr_id: str, timeout: int = 300) -> None: """Assert Spot Fleet request is cancelled/deleted or has zero target capacity.""" deadline = time.time() + timeout last_state = None terminal_states = {"cancelled", "cancelled_running", "cancelled_terminating", "failed"} while time.time() < deadline: try: - resp = ec2_client.describe_spot_fleet_requests(SpotFleetRequestIds=[sfr_id]) + resp = _get_ec2_client().describe_spot_fleet_requests(SpotFleetRequestIds=[sfr_id]) configs = resp.get("SpotFleetRequestConfigs", []) if not configs: log.info("Spot Fleet %s: not found (deleted)", sfr_id) @@ -239,6 +335,7 @@ def _assert_spot_fleet_deleted(sfr_id: str, timeout: int = 120) -> None: if "NotFound" in str(exc) or "InvalidSpotFleetRequestId" in str(exc): log.info("Spot Fleet %s: not found (deleted)", sfr_id) return + last_state = f"describe_error={type(exc).__name__}: {exc}" log.warning("_assert_spot_fleet_deleted: describe failed for %s: %s", sfr_id, exc) time.sleep(10) pytest.fail( @@ -249,7 +346,7 @@ def _assert_spot_fleet_deleted(sfr_id: str, timeout: int = 120) -> None: def _assert_launch_templates_deleted(request_id: str) -> None: """Assert no launch templates tagged orb:request-id= exist.""" try: - resp = ec2_client.describe_launch_templates( + resp = _get_ec2_client().describe_launch_templates( Filters=[{"Name": "tag:orb:request-id", "Values": [request_id]}] ) templates = resp.get("LaunchTemplates", []) @@ -285,7 +382,7 @@ def _extract_resource_ids(result) -> list[str]: def _get_machine_ids_from_ec2(request_id: str) -> list[str]: - return _get_machine_ids_from_ec2_helper(request_id, ec2_client) + return _get_machine_ids_from_ec2_helper(request_id, _get_ec2_client()) # --------------------------------------------------------------------------- @@ -316,15 +413,16 @@ def setup_cleanup_e2e(request, test_session_id): (test_config_dir / "logs").mkdir(exist_ok=True) (test_config_dir / "work").mkdir(exist_ok=True) - os.environ["ORB_CONFIG_DIR"] = str(test_config_dir) - os.environ["HF_PROVIDER_CONFDIR"] = str(test_config_dir) + config_dir = test_config_dir / "config" + os.environ["ORB_CONFIG_DIR"] = str(config_dir) + os.environ["HF_PROVIDER_CONFDIR"] = str(config_dir) os.environ["HF_PROVIDER_LOGDIR"] = str(test_config_dir / "logs") os.environ["HF_PROVIDER_WORKDIR"] = str(test_config_dir / "work") os.environ["DEFAULT_PROVIDER_WORKDIR"] = str(test_config_dir / "work") os.environ["AWS_PROVIDER_LOG_DIR"] = str(test_config_dir / "logs") os.environ["HF_LOGDIR"] = str(test_config_dir / "logs") - config_path = str(test_config_dir / "config.json") + config_path = str(config_dir / "config.json") _tracked_request_ids: list[str] = [] @@ -377,18 +475,29 @@ async def _cleanup() -> None: all_machine_ids = [ mid for req_id in _tracked_request_ids for mid in _get_machine_ids_from_ec2(req_id) ] - wait_for_instances_terminated(all_machine_ids, ec2_client) + wait_for_instances_terminated(all_machine_ids, _get_ec2_client()) except Exception as exc: log.warning("Fixture teardown: wait_for_instances_terminated failed: %s", exc) for req_id in _tracked_request_ids: try: - cleanup_launch_templates_for_request(req_id, ec2_client) + cleanup_launch_templates_for_request(req_id, _get_ec2_client()) except Exception as exc: log.warning( "Fixture teardown: cleanup_launch_templates failed for %s: %s", req_id, exc ) + for key in ( + "ORB_CONFIG_DIR", + "HF_PROVIDER_CONFDIR", + "HF_PROVIDER_LOGDIR", + "HF_PROVIDER_WORKDIR", + "DEFAULT_PROVIDER_WORKDIR", + "AWS_PROVIDER_LOG_DIR", + "HF_LOGDIR", + ): + os.environ.pop(key, None) + try: from orb.infrastructure.di import reset_container @@ -455,10 +564,29 @@ async def _run_cleanup_verification( # 3. Assert instances provisioned machine_ids = _extract_machine_ids(status_response) - assert len(machine_ids) == capacity, ( - f"Expected {capacity} machines, got {len(machine_ids)}: {machine_ids}" + + # Capacity-unit fulfilment check from provider response fields. + # target_units / fulfilled_units reflect capacity units for weighted fleets + # and instance count for unweighted templates (1 unit == 1 instance). + _req0 = status_response.get("requests", [{}])[0] if isinstance(status_response, dict) else {} + target_units = _req0["target_units"] if _req0.get("target_units") is not None else capacity + fulfilled_units = ( + _req0["fulfilled_units"] if _req0.get("fulfilled_units") is not None else len(machine_ids) + ) + assert fulfilled_units >= target_units, ( + f"Fleet not fully fulfilled: fulfilled={fulfilled_units}, target={target_units}" ) + # Template-aware instance count sanity check. + if scenarios.template_uses_weighted_capacity(test_case): + assert len(machine_ids) >= 1, ( + f"Expected at least 1 machine (weighted template), got: {machine_ids}" + ) + else: + assert len(machine_ids) == capacity, ( + f"Expected {capacity} machines (unweighted template), got {len(machine_ids)}: {machine_ids}" + ) + returned_id = status_response.get("requests", [{}])[0].get("request_id") or status_response.get( "requests", [{}] )[0].get("requestId") @@ -489,33 +617,27 @@ async def _run_cleanup_verification( return_request_id = _extract_request_id(return_result) - # 6. Poll return completion + # 6. Poll return completion via get_request_status(return_request_id). + # The HostFactory getReturnRequests wire op is for provider-initiated + # retirements (preempted/deleted machines), not user-initiated returns — + # `get_request_status(return_id)` is the correct API for "is this return + # complete?" under any scheduler. if return_request_id: deadline = time.time() + SDK_TIMEOUTS["return_completion"] + terminal = {"complete", "complete_with_error", "failed", "cancelled", "timeout"} while True: - ret_status = await sdk.list_return_requests() # type: ignore[attr-defined] - requests = ( - ret_status.get("requests", []) if isinstance(ret_status, dict) else ret_status or [] - ) - done = False - for req in requests: - rid = ( - req.get("requestId") or req.get("request_id") - if isinstance(req, dict) - else getattr(req, "request_id", None) - ) - s = req.get("status") if isinstance(req, dict) else getattr(req, "status", None) - if rid == return_request_id and s == "complete": - done = True - break - if done: + status_response = await sdk.get_request_status(request_id=return_request_id) # type: ignore[attr-defined] + status = _extract_request_status(status_response) + if status in terminal: + if status != "complete": + pytest.fail(f"Return request ended with non-success status: {status}") break if time.time() > deadline: pytest.fail("Timed out waiting for return request to complete") await asyncio.sleep(SDK_TIMEOUTS["poll_interval"]) # 7. Wait for instance termination - wait_for_instances_terminated(machine_ids, ec2_client, timeout=300) + wait_for_instances_terminated(machine_ids, _get_ec2_client(), timeout=300) # 8. Assert all instances terminated all_terminated = _check_all_ec2_hosts_are_being_terminated(machine_ids) @@ -528,7 +650,10 @@ async def _run_cleanup_verification( # 9. Assert backing resource deleted / zeroed if resource_id: if provider_api == "ASG": - _assert_asg_deleted(resource_id) + _assert_asg_deleted( + resource_id, + weighted=scenarios.template_uses_weighted_capacity(test_case), + ) elif provider_api == "EC2Fleet": _assert_fleet_deleted(resource_id) elif provider_api == "SpotFleet": @@ -652,53 +777,46 @@ async def test_run_instances_cleanup(self, setup_cleanup_e2e): f"Expected {capacity} machines, got {len(machine_ids)}: {machine_ids}" ) for machine_id in machine_ids: - state = get_instance_state(machine_id) - assert state["exists"], f"Instance {machine_id} not found in AWS" - assert state["state"] in ("running", "pending"), ( - f"Instance {machine_id} in unexpected state: {state['state']}" - ) + try: + resp = _get_ec2_client().describe_instances(InstanceIds=[machine_id]) + inst_state = resp["Reservations"][0]["Instances"][0]["State"]["Name"] + assert inst_state in ("running", "pending"), ( + f"Instance {machine_id} in unexpected state: {inst_state}" + ) + except ClientError as exc: + if exc.response["Error"]["Code"] == "InvalidInstanceID.NotFound": + pytest.fail(f"Instance {machine_id} not found in AWS") + raise log.info("All %d RunInstances instances provisioned: %s", capacity, machine_ids) # 4. Return ALL machines return_result = await sdk.create_return_request(machine_ids=machine_ids) # type: ignore[attr-defined] return_request_id = _extract_request_id(return_result) - # 5. Poll return completion + # 5. Poll return completion via get_request_status(return_request_id). + # See note above: list_return_requests maps to HF getReturnRequests + # wire op which is for provider-initiated retirements. if return_request_id: deadline = time.time() + SDK_TIMEOUTS["return_completion"] + terminal = {"complete", "complete_with_error", "failed", "cancelled", "timeout"} while True: - ret_status = await sdk.list_return_requests() # type: ignore[attr-defined] - requests = ( - ret_status.get("requests", []) - if isinstance(ret_status, dict) - else ret_status or [] + status_response = await sdk.get_request_status( # type: ignore[attr-defined] + request_id=return_request_id ) - done = False - for req in requests: - rid = ( - req.get("requestId") or req.get("request_id") - if isinstance(req, dict) - else getattr(req, "request_id", None) - ) - s = ( - req.get("status") - if isinstance(req, dict) - else getattr(req, "status", None) - ) - if rid == return_request_id and s == "complete": - done = True - break - if done: + status = _extract_request_status(status_response) + if status in terminal: + if status != "complete": + pytest.fail(f"Return request ended with non-success status: {status}") break if time.time() > deadline: pytest.fail("Timed out waiting for RunInstances return request to complete") await asyncio.sleep(SDK_TIMEOUTS["poll_interval"]) # 6. Wait for instance termination - wait_for_instances_terminated(machine_ids, ec2_client, timeout=300) + wait_for_instances_terminated(machine_ids, _get_ec2_client(), timeout=300) # 7. Assert no instances remain in running state via describe_instances - resp = ec2_client.describe_instances( + resp = _get_ec2_client().describe_instances( InstanceIds=machine_ids, Filters=[{"Name": "instance-state-name", "Values": ["running", "pending"]}], ) diff --git a/tests/onaws/test_mcp_onaws.py b/tests/providers/aws/live/test_mcp_onaws.py similarity index 77% rename from tests/onaws/test_mcp_onaws.py rename to tests/providers/aws/live/test_mcp_onaws.py index 32e342d9a..80b7f93c3 100644 --- a/tests/onaws/test_mcp_onaws.py +++ b/tests/providers/aws/live/test_mcp_onaws.py @@ -13,16 +13,16 @@ import boto3.session import pytest -from tests.onaws import scenarios -from tests.onaws.cleanup_helpers import ( +from tests.providers.aws.live import scenarios +from tests.providers.aws.live.cleanup_helpers import ( cleanup_launch_templates_for_request, get_machine_ids_from_ec2 as _get_machine_ids_from_ec2_helper, wait_for_instances_terminated, ) -from tests.onaws.template_processor import TemplateProcessor +from tests.providers.aws.live.template_processor import TemplateProcessor try: - from tests.onaws.scenarios_mcp import ( # type: ignore[import] + from tests.providers.aws.live.scenarios_mcp import ( # type: ignore[import] MCP_RUN_DEFAULT_COMBINATIONS, MCP_TIMEOUTS, ) @@ -32,7 +32,7 @@ # Import AWS validation helpers from test_onaws (guarded for env/creds issues) try: - from tests.onaws.test_onaws import ( + from tests.providers.aws.live.test_onaws import ( _check_all_ec2_hosts_are_being_terminated, get_instance_state, ) @@ -54,14 +54,46 @@ os.environ["LOG_DESTINATION"] = "file" os.environ.setdefault("AWS_REGION", "eu-west-1") -_boto_session = boto3.session.Session() -_ec2_region = ( - os.environ.get("AWS_REGION") - or os.environ.get("AWS_DEFAULT_REGION") - or _boto_session.region_name - or "eu-west-1" -) -ec2_client = _boto_session.client("ec2", region_name=_ec2_region) +_ec2_client = None + + +def _get_boto_profile_and_region() -> tuple[str | None, str]: + """Read AWS profile and region from ORB_CONFIG_DIR config.""" + import json as _json + + profile = None + region = None + config_dir = os.environ.get("ORB_CONFIG_DIR") + if config_dir: + try: + config_path = os.path.join(config_dir, "config.json") + with open(config_path) as _f: + config = _json.load(_f) + providers = config.get("provider", {}).get("providers", []) + if providers: + provider_cfg = providers[0].get("config", {}) + profile = provider_cfg.get("profile") + region = provider_cfg.get("region") + except Exception: + pass + region = ( + region + or os.environ.get("AWS_REGION") + or os.environ.get("AWS_DEFAULT_REGION") + or "eu-west-1" + ) + return profile, region + + +def _get_ec2_client(): + global _ec2_client + if _ec2_client is None: + _profile, _region = _get_boto_profile_and_region() + _ec2_client = boto3.session.Session(profile_name=_profile, region_name=_region).client( + "ec2", region_name=_region + ) + return _ec2_client + log = logging.getLogger("mcp_test") log.setLevel(logging.DEBUG) @@ -83,6 +115,11 @@ @pytest.fixture def setup_mcp_test(request, test_session_id): """Generate per-test config dir, set env vars, construct MCP server, yield, teardown.""" + import asyncio + + from orb.bootstrap import Application + from orb.infrastructure.di import reset_container + from orb.infrastructure.di.container import get_container from orb.interface.mcp.server.core import OpenResourceBrokerMCPServer processor = TemplateProcessor() @@ -106,15 +143,24 @@ def setup_mcp_test(request, test_session_id): (test_config_dir / "logs").mkdir(exist_ok=True) (test_config_dir / "work").mkdir(exist_ok=True) - os.environ["ORB_CONFIG_DIR"] = str(test_config_dir) - os.environ["HF_PROVIDER_CONFDIR"] = str(test_config_dir) + config_dir = test_config_dir / "config" + os.environ["ORB_CONFIG_DIR"] = str(config_dir) + os.environ["HF_PROVIDER_CONFDIR"] = str(config_dir) os.environ["HF_PROVIDER_LOGDIR"] = str(test_config_dir / "logs") os.environ["HF_PROVIDER_WORKDIR"] = str(test_config_dir / "work") os.environ["DEFAULT_PROVIDER_WORKDIR"] = str(test_config_dir / "work") os.environ["AWS_PROVIDER_LOG_DIR"] = str(test_config_dir / "logs") os.environ["HF_LOGDIR"] = str(test_config_dir / "logs") - mcp_server = OpenResourceBrokerMCPServer(app=None) + # Bootstrap ORB so the DI container has providers, CQRS handlers, + # configuration etc. registered before MCP tool handlers run. Without + # this every MCP request fails with "No strategy found for provider". + reset_container() + app = Application(config_path=str(config_dir / "config.json"), skip_validation=True) + if not asyncio.run(app.initialize()): + raise RuntimeError("Failed to initialize ORB application for MCP test") + + mcp_server = OpenResourceBrokerMCPServer(app=get_container()) _tracked_request_ids: list[str] = [] @@ -169,20 +215,31 @@ async def _cleanup() -> None: for req_id in _tracked_request_ids for mid in _get_machine_ids_from_ec2(req_id) ], - ec2_client, + _get_ec2_client(), ) except Exception as exc: log.warning("Fixture teardown: wait_for_instances_terminated failed: %s", exc) for req_id in _tracked_request_ids: try: - cleanup_launch_templates_for_request(req_id, ec2_client) + cleanup_launch_templates_for_request(req_id, _get_ec2_client()) except Exception as exc: log.warning( "Fixture teardown: cleanup_launch_templates failed for %s: %s", req_id, exc ) - # Teardown: reset DI container so next test gets a fresh one + # Teardown: clean up env vars and reset DI container so next test gets a fresh one + for key in ( + "ORB_CONFIG_DIR", + "HF_PROVIDER_CONFDIR", + "HF_PROVIDER_LOGDIR", + "HF_PROVIDER_WORKDIR", + "DEFAULT_PROVIDER_WORKDIR", + "AWS_PROVIDER_LOG_DIR", + "HF_LOGDIR", + ): + os.environ.pop(key, None) + try: from orb.infrastructure.di import reset_container @@ -199,7 +256,7 @@ async def _cleanup() -> None: def _get_machine_ids_from_ec2(request_id: str) -> list[str]: - return _get_machine_ids_from_ec2_helper(request_id, ec2_client) + return _get_machine_ids_from_ec2_helper(request_id, _get_ec2_client()) async def _call_tool(mcp_server, tool_name: str, arguments: dict, msg_id: int = 1) -> dict: @@ -318,17 +375,36 @@ async def _run_full_cycle_mcp(mcp_server, test_case: dict, tracked_request_ids: ) machine_ids = _extract_machine_ids(status_result) - assert len(machine_ids) == capacity, ( - f"Expected {capacity} machine(s), got {len(machine_ids)}: {machine_ids}" + + # Capacity-unit fulfilment check from provider response fields. + # target_units / fulfilled_units reflect capacity units for weighted fleets + # and instance count for unweighted templates (1 unit == 1 instance). + _req0 = status_result.get("requests", [{}])[0] if isinstance(status_result, dict) else {} + target_units = _req0["target_units"] if _req0.get("target_units") is not None else capacity + fulfilled_units = ( + _req0["fulfilled_units"] if _req0.get("fulfilled_units") is not None else len(machine_ids) + ) + assert fulfilled_units >= target_units, ( + f"Fleet not fully fulfilled: fulfilled={fulfilled_units}, target={target_units}" ) + # Template-aware instance count sanity check. + if scenarios.template_uses_weighted_capacity(test_case): + assert len(machine_ids) >= 1, ( + f"Expected at least 1 machine (weighted template), got: {machine_ids}" + ) + else: + assert len(machine_ids) == capacity, ( + f"Expected {capacity} machines (unweighted template), got {len(machine_ids)}: {machine_ids}" + ) + for machine_id in machine_ids: state = get_instance_state(machine_id) assert state["exists"], f"Instance {machine_id} not found in AWS" assert state["state"] in ("running", "pending"), ( f"Instance {machine_id} in unexpected state: {state['state']}" ) - log.info("All %d instance(s) provisioned: %s", capacity, machine_ids) + log.info("All %d instance(s) provisioned: %s", len(machine_ids), machine_ids) # 4. Return machines return_result = await _call_tool(mcp_server, "return_machines", {"machine_ids": machine_ids}) @@ -336,19 +412,21 @@ async def _run_full_cycle_mcp(mcp_server, test_case: dict, tracked_request_ids: return_request_id = _extract_request_id(return_result) - # 5. Poll return completion via list_return_requests + # 5. Poll return completion via get_request_status(return_request_id). + # The HostFactory getReturnRequests wire format flattens per-machine items + # without a per-request status, so the right way to ask "is this return + # complete?" under any scheduler is get_request_status(return_id). if return_request_id: deadline = time.time() + MCP_TIMEOUTS["return_completion"] + terminal = {"complete", "complete_with_error", "failed", "cancelled", "timeout"} while True: - list_result = await _call_tool(mcp_server, "list_return_requests", {}) - requests = list_result.get("requests", []) - done = any( - (req.get("requestId") or req.get("request_id")) == return_request_id - and req.get("status") == "complete" - for req in requests - if isinstance(req, dict) + status_resp = await _call_tool( + mcp_server, "get_request_status", {"request_id": return_request_id} ) - if done: + status = _extract_request_status(status_resp) + if status in terminal: + if status != "complete": + pytest.fail(f"Return request ended with non-success status: {status}") break if time.time() > deadline: pytest.fail("Timed out waiting for return request to complete") diff --git a/tests/onaws/test_multi_asg_termination.py b/tests/providers/aws/live/test_multi_asg_termination.py similarity index 86% rename from tests/onaws/test_multi_asg_termination.py rename to tests/providers/aws/live/test_multi_asg_termination.py index 6eb26a418..2d25e2a43 100644 --- a/tests/onaws/test_multi_asg_termination.py +++ b/tests/providers/aws/live/test_multi_asg_termination.py @@ -1,6 +1,7 @@ import json import logging import os +import re import time import uuid from pathlib import Path @@ -12,11 +13,11 @@ from jsonschema import ValidationError, validate as validate_json_schema from hfmock import HostFactoryMock -from tests.onaws import plugin_io_schemas -from tests.onaws.cleanup_helpers import cleanup_tracked_requests -from tests.onaws.parse_output import parse_and_print_output -from tests.onaws.template_processor import TemplateProcessor -from tests.onaws.test_onaws import get_instances_states +from tests.providers.aws.live import plugin_io_schemas +from tests.providers.aws.live.cleanup_helpers import cleanup_tracked_requests +from tests.providers.aws.live.parse_output import parse_and_print_output +from tests.providers.aws.live.template_processor import TemplateProcessor +from tests.providers.aws.live.test_onaws import get_instances_states pytestmark = [ # Apply default markers to every test in this module pytest.mark.manual_aws, @@ -31,15 +32,61 @@ os.environ["AWS_PROVIDER_LOG_DIR"] = "./logs" os.environ["LOG_DESTINATION"] = "file" -_boto_session = boto3.Session() -_ec2_region = ( - os.environ.get("AWS_REGION") - or os.environ.get("AWS_DEFAULT_REGION") - or _boto_session.region_name - or "eu-west-1" -) -ec2_client = _boto_session.client("ec2", region_name=_ec2_region) -autoscaling_client = _boto_session.client("autoscaling", region_name=_ec2_region) +_ec2_client = None +_autoscaling_client = None + + +def _get_boto_profile_and_region() -> tuple[str | None, str]: + """Read AWS profile and region from ORB_CONFIG_DIR config, same as test_onaws._get_boto_clients.""" + import json as _json + + profile = None + region = None + config_dir = os.environ.get("ORB_CONFIG_DIR") + if config_dir: + try: + config_path = os.path.join(config_dir, "config.json") + with open(config_path) as _f: + config = _json.load(_f) + providers = config.get("provider", {}).get("providers", []) + if providers: + provider_cfg = providers[0].get("config", {}) + profile = provider_cfg.get("profile") + region = provider_cfg.get("region") + except Exception: + pass + # Fall back to AWS_PROFILE env var so the explicit profile_name is always + # non-None when a profile is available. This forces botocore to use + # profile-based credentials and ignore injected fake env var credentials. + profile = profile or os.environ.get("AWS_PROFILE") + region = ( + region + or os.environ.get("AWS_REGION") + or os.environ.get("AWS_DEFAULT_REGION") + or "eu-west-1" + ) + return profile, region + + +def _get_ec2_client(): + global _ec2_client + if _ec2_client is None: + _profile, _region = _get_boto_profile_and_region() + _ec2_client = boto3.Session(profile_name=_profile, region_name=_region).client( + "ec2", region_name=_region + ) + return _ec2_client + + +def _get_autoscaling_client(): + global _autoscaling_client + if _autoscaling_client is None: + _profile, _region = _get_boto_profile_and_region() + _autoscaling_client = boto3.Session(profile_name=_profile, region_name=_region).client( + "autoscaling", region_name=_region + ) + return _autoscaling_client + log = logging.getLogger("multi_asg_test") log.setLevel(logging.DEBUG) @@ -52,12 +99,15 @@ log.addHandler(console_handler) MAX_TIME_WAIT_FOR_CAPACITY_PROVISIONING_SEC = 180 +_IPV4_RE = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$") def get_asg_instances(asg_name: str) -> List[str]: """Get all instance IDs from an ASG.""" try: - response = autoscaling_client.describe_auto_scaling_groups(AutoScalingGroupNames=[asg_name]) + response = _get_autoscaling_client().describe_auto_scaling_groups( + AutoScalingGroupNames=[asg_name] + ) if not response["AutoScalingGroups"]: return [] @@ -74,7 +124,9 @@ def verify_asg_instances_detached(instance_ids: List[str]) -> bool: if not instance_ids: return True - response = autoscaling_client.describe_auto_scaling_instances(InstanceIds=instance_ids) + response = _get_autoscaling_client().describe_auto_scaling_instances( + InstanceIds=instance_ids + ) # If any instances are still in ASGs, they will appear in the response remaining_asg_instances = response.get("AutoScalingInstances", []) @@ -103,7 +155,7 @@ def verify_asg_instances_detached(instance_ids: List[str]) -> bool: def check_all_instances_terminating_or_terminated(instance_ids: List[str]) -> bool: """Check if all instances are in terminating or terminated state.""" all_terminating = True - instance_states = get_instances_states(instance_ids, ec2_client) + instance_states = get_instances_states(instance_ids, _get_ec2_client()) for instance_id, state in zip(instance_ids, instance_states): if state is not None: @@ -160,7 +212,9 @@ def setup_multi_asg_templates(test_session_id): processor.generate_combined_templates(test_name, template_configs) # Set environment variables - os.environ["HF_PROVIDER_CONFDIR"] = str(test_config_dir) + config_dir = test_config_dir / "config" + os.environ["ORB_CONFIG_DIR"] = str(config_dir) + os.environ["HF_PROVIDER_CONFDIR"] = str(config_dir) os.environ["HF_PROVIDER_LOGDIR"] = str(test_config_dir / "logs") os.environ["HF_PROVIDER_WORKDIR"] = str(test_config_dir / "work") os.environ["AWS_PROVIDER_LOG_DIR"] = str(test_config_dir / "logs") @@ -186,10 +240,19 @@ def _tracking_request_machines(template_name: str, machine_count: int): yield hfm, template_configs try: - cleanup_tracked_requests(_tracked_request_ids, hfm, ec2_client) + cleanup_tracked_requests(_tracked_request_ids, hfm, _get_ec2_client()) except Exception as exc: log.warning("Fixture teardown: cleanup_tracked_requests failed: %s", exc) + for key in ( + "ORB_CONFIG_DIR", + "HF_PROVIDER_CONFDIR", + "HF_PROVIDER_LOGDIR", + "HF_PROVIDER_WORKDIR", + "AWS_PROVIDER_LOG_DIR", + "HF_LOGDIR", + ): + os.environ.pop(key, None) processor.cleanup_test_templates(test_name) log.removeHandler(file_handler) file_handler.close() @@ -272,7 +335,7 @@ def provision_asg_capacity( # dead code - commented out # machines = status_response["requests"][0]["machines"] # # instance_ids = [machine.get("machineId") or machine.get("machine_id") for machine in machines] - # instance_states = get_instances_states(instance_ids, ec2_client) + # instance_states = get_instances_states(instance_ids, _get_ec2_client()) # # for machine, state in zip(machines, instance_states): # assert machine["status"] in ["running", "pending"] @@ -468,11 +531,17 @@ def test_multi_asg_termination(setup_multi_asg_templates): instance_ids = [ machine.get("machineId") or machine.get("machine_id") for machine in machines ] - instance_states = get_instances_states(instance_ids, ec2_client) + instance_states = get_instances_states(instance_ids, _get_ec2_client()) for machine, state in zip(machines, instance_states): assert machine["status"] in ["running", "pending"] instance_id = machine.get("machineId") or machine.get("machine_id") + if machine["status"] == "running": + ip = machine.get("privateIpAddress") or machine.get("private_ip_address") + assert ip, f"Running machine {instance_id} has empty privateIpAddress" + assert _IPV4_RE.match(ip), ( + f"Running machine {instance_id} has invalid privateIpAddress: {ip!r}" + ) assert state is not None assert state in ["running", "pending"] log.debug(f"EC2 {instance_id} state: {state}") @@ -494,7 +563,9 @@ def test_multi_asg_termination(setup_multi_asg_templates): # Get ASG names from the instances asg_names = set() try: - response = autoscaling_client.describe_auto_scaling_instances(InstanceIds=all_instance_ids) + response = _get_autoscaling_client().describe_auto_scaling_instances( + InstanceIds=all_instance_ids + ) for instance_info in response.get("AutoScalingInstances", []): asg_name = instance_info.get("AutoScalingGroupName") @@ -571,7 +642,7 @@ def test_multi_asg_termination(setup_multi_asg_templates): def check_asg_exists(asg_name: str) -> bool: """Check if an ASG still exists.""" try: - response = autoscaling_client.describe_auto_scaling_groups( + response = _get_autoscaling_client().describe_auto_scaling_groups( AutoScalingGroupNames=[asg_name] ) return len(response.get("AutoScalingGroups", [])) > 0 @@ -596,7 +667,7 @@ def check_asg_exists(asg_name: str) -> bool: # Log current ASG status for debugging try: - response = autoscaling_client.describe_auto_scaling_groups( + response = _get_autoscaling_client().describe_auto_scaling_groups( AutoScalingGroupNames=[asg_name] ) if response.get("AutoScalingGroups"): @@ -631,7 +702,7 @@ def check_asg_exists(asg_name: str) -> bool: # Log detailed information about remaining ASGs for debugging for asg_name in final_remaining_asgs: try: - response = autoscaling_client.describe_auto_scaling_groups( + response = _get_autoscaling_client().describe_auto_scaling_groups( AutoScalingGroupNames=[asg_name] ) if response.get("AutoScalingGroups"): diff --git a/tests/onaws/test_multi_ec2_fleet_termination.py b/tests/providers/aws/live/test_multi_ec2_fleet_termination.py similarity index 84% rename from tests/onaws/test_multi_ec2_fleet_termination.py rename to tests/providers/aws/live/test_multi_ec2_fleet_termination.py index 5115784fe..ec3952332 100644 --- a/tests/onaws/test_multi_ec2_fleet_termination.py +++ b/tests/providers/aws/live/test_multi_ec2_fleet_termination.py @@ -1,6 +1,7 @@ import json import logging import os +import re import time import uuid from pathlib import Path @@ -12,11 +13,11 @@ from jsonschema import ValidationError, validate as validate_json_schema from hfmock import HostFactoryMock -from tests.onaws import plugin_io_schemas -from tests.onaws.cleanup_helpers import cleanup_tracked_requests -from tests.onaws.parse_output import parse_and_print_output -from tests.onaws.template_processor import TemplateProcessor -from tests.onaws.test_onaws import get_instances_states +from tests.providers.aws.live import plugin_io_schemas +from tests.providers.aws.live.cleanup_helpers import cleanup_tracked_requests +from tests.providers.aws.live.parse_output import parse_and_print_output +from tests.providers.aws.live.template_processor import TemplateProcessor +from tests.providers.aws.live.test_onaws import get_instances_states pytestmark = [ # Apply default markers to every test in this module pytest.mark.manual_aws, @@ -31,14 +32,50 @@ os.environ["AWS_PROVIDER_LOG_DIR"] = "./logs" os.environ["LOG_DESTINATION"] = "file" -_boto_session = boto3.Session() -_ec2_region = ( - os.environ.get("AWS_REGION") - or os.environ.get("AWS_DEFAULT_REGION") - or _boto_session.region_name - or "eu-west-1" -) -ec2_client = _boto_session.client("ec2", region_name=_ec2_region) +_ec2_client = None + + +def _get_boto_profile_and_region() -> tuple[str | None, str]: + """Read AWS profile and region from ORB_CONFIG_DIR config, same as test_onaws._get_boto_clients.""" + import json as _json + + profile = None + region = None + config_dir = os.environ.get("ORB_CONFIG_DIR") + if config_dir: + try: + config_path = os.path.join(config_dir, "config.json") + with open(config_path) as _f: + config = _json.load(_f) + providers = config.get("provider", {}).get("providers", []) + if providers: + provider_cfg = providers[0].get("config", {}) + profile = provider_cfg.get("profile") + region = provider_cfg.get("region") + except Exception: + pass + # Fall back to AWS_PROFILE env var so the explicit profile_name is always + # non-None when a profile is available. This forces botocore to use + # profile-based credentials and ignore injected fake env var credentials. + profile = profile or os.environ.get("AWS_PROFILE") + region = ( + region + or os.environ.get("AWS_REGION") + or os.environ.get("AWS_DEFAULT_REGION") + or "eu-west-1" + ) + return profile, region + + +def _get_ec2_client(): + global _ec2_client + if _ec2_client is None: + _profile, _region = _get_boto_profile_and_region() + _ec2_client = boto3.Session(profile_name=_profile, region_name=_region).client( + "ec2", region_name=_region + ) + return _ec2_client + log = logging.getLogger("multi_ec2_fleet_test") log.setLevel(logging.DEBUG) @@ -51,28 +88,42 @@ log.addHandler(console_handler) MAX_TIME_WAIT_FOR_CAPACITY_PROVISIONING_SEC = 180 +_IPV4_RE = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$") def get_ec2_fleet_instances(fleet_id: str) -> List[str]: """Get all instance IDs from an EC2 Fleet.""" try: - response = ec2_client.describe_fleet_instances(FleetId=fleet_id) + response = _get_ec2_client().describe_fleet_instances(FleetId=fleet_id) return [instance["InstanceId"] for instance in response.get("ActiveInstances", [])] except ClientError as e: log.error(f"Error getting EC2 Fleet instances for {fleet_id}: {e}") return [] -def verify_ec2_fleet_instances_detached(instance_ids: List[str]) -> bool: - """Verify that instances are no longer part of any EC2 Fleet.""" +def verify_ec2_fleet_instances_detached( + instance_ids: List[str], + fleet_ids: List[str] | None = None, +) -> bool: + """Verify that instances are no longer part of any EC2 Fleet. + + When *fleet_ids* is supplied the check is scoped to those specific fleets + (the ones created by this test run). Without it the call falls back to + listing every active fleet in the region, which can be slow and may + include stale fleets from earlier test runs. + """ try: if not instance_ids: return True - # Get all active EC2 fleets - response = ec2_client.describe_fleets( - Filters=[{"Name": "fleet-state", "Values": ["active", "modifying"]}] - ) + if fleet_ids: + # Fast path: only inspect the fleets we created in this test. + response = _get_ec2_client().describe_fleets(FleetIds=list(fleet_ids)) + else: + # Fallback: scan every active fleet in the region. + response = _get_ec2_client().describe_fleets( + Filters=[{"Name": "fleet-state", "Values": ["active", "modifying"]}] + ) # Check each fleet for our instances for fleet in response.get("Fleets", []): @@ -81,7 +132,7 @@ def verify_ec2_fleet_instances_detached(instance_ids: List[str]) -> bool: continue try: - fleet_instances = ec2_client.describe_fleet_instances(FleetId=fleet_id) + fleet_instances = _get_ec2_client().describe_fleet_instances(FleetId=fleet_id) active_instance_ids = [ inst["InstanceId"] for inst in fleet_instances.get("ActiveInstances", []) ] @@ -114,7 +165,7 @@ def verify_ec2_fleet_instances_detached(instance_ids: List[str]) -> bool: def check_all_instances_terminating_or_terminated(instance_ids: List[str]) -> bool: """Check if all instances are in terminating or terminated state.""" all_terminating = True - instance_states = get_instances_states(instance_ids, ec2_client) + instance_states = get_instances_states(instance_ids, _get_ec2_client()) for instance_id, state in zip(instance_ids, instance_states): if state is not None: @@ -177,7 +228,9 @@ def setup_multi_ec2_fleet_templates(test_session_id): processor.generate_combined_templates(test_name, template_configs) # Set environment variables - os.environ["HF_PROVIDER_CONFDIR"] = str(test_config_dir) + config_dir = test_config_dir / "config" + os.environ["ORB_CONFIG_DIR"] = str(config_dir) + os.environ["HF_PROVIDER_CONFDIR"] = str(config_dir) os.environ["HF_PROVIDER_LOGDIR"] = str(test_config_dir / "logs") os.environ["HF_PROVIDER_WORKDIR"] = str(test_config_dir / "work") os.environ["AWS_PROVIDER_LOG_DIR"] = str(test_config_dir / "logs") @@ -203,10 +256,19 @@ def _tracking_request_machines(template_name: str, machine_count: int): yield hfm, template_configs try: - cleanup_tracked_requests(_tracked_request_ids, hfm, ec2_client) + cleanup_tracked_requests(_tracked_request_ids, hfm, _get_ec2_client()) except Exception as exc: log.warning("Fixture teardown: cleanup_tracked_requests failed: %s", exc) + for key in ( + "ORB_CONFIG_DIR", + "HF_PROVIDER_CONFDIR", + "HF_PROVIDER_LOGDIR", + "HF_PROVIDER_WORKDIR", + "AWS_PROVIDER_LOG_DIR", + "HF_LOGDIR", + ): + os.environ.pop(key, None) processor.cleanup_test_templates(test_name) log.removeHandler(file_handler) file_handler.close() @@ -289,7 +351,7 @@ def provision_ec2_fleet_capacity( # dead code - commented out # machines = status_response["requests"][0]["machines"] # # instance_ids = [machine.get("machineId") or machine.get("machine_id") for machine in machines] - # instance_states = get_instances_states(instance_ids, ec2_client) + # instance_states = get_instances_states(instance_ids, _get_ec2_client()) # # for machine, state in zip(machines, instance_states): # assert machine["status"] in ["running", "pending"] @@ -488,11 +550,17 @@ def test_multi_ec2_fleet_termination(setup_multi_ec2_fleet_templates): instance_ids = [ machine.get("machineId") or machine.get("machine_id") for machine in machines ] - instance_states = get_instances_states(instance_ids, ec2_client) + instance_states = get_instances_states(instance_ids, _get_ec2_client()) for machine, state in zip(machines, instance_states): assert machine["status"] in ["running", "pending"] instance_id = machine.get("machineId") or machine.get("machine_id") + if machine["status"] == "running": + ip = machine.get("privateIpAddress") or machine.get("private_ip_address") + assert ip, f"Running machine {instance_id} has empty privateIpAddress" + assert _IPV4_RE.match(ip), ( + f"Running machine {instance_id} has invalid privateIpAddress: {ip!r}" + ) assert state is not None assert state in ["running", "pending"] log.debug(f"EC2 {instance_id} state: {state}") @@ -514,7 +582,7 @@ def test_multi_ec2_fleet_termination(setup_multi_ec2_fleet_templates): # Get EC2 Fleet IDs from the instances by checking tags ec2_fleet_ids = set() try: - response = ec2_client.describe_instances(InstanceIds=all_instance_ids) + response = _get_ec2_client().describe_instances(InstanceIds=all_instance_ids) for reservation in response.get("Reservations", []): for instance in reservation.get("Instances", []): @@ -533,8 +601,13 @@ def test_multi_ec2_fleet_termination(setup_multi_ec2_fleet_templates): pytest.fail(f"Failed to verify EC2 Fleet membership: {e}") log.info(f"Found instances in {len(ec2_fleet_ids)} EC2 Fleets: {list(ec2_fleet_ids)}") - assert len(ec2_fleet_ids) == 2, ( - f"Expected instances in 2 EC2 Fleets, found {len(ec2_fleet_ids)}" + # We provisioned from 2 templates, so we expect at least 2 fleet IDs. + # An instant EC2 Fleet with lowestPrice allocation may launch instances from + # multiple capacity pools, each tagged with its own aws:ec2:fleet-id. The + # exact count therefore varies; what matters is that we got fleets from both + # templates (>= 2) and that subsequent termination handles all of them. + assert len(ec2_fleet_ids) >= 2, ( + f"Expected instances in at least 2 EC2 Fleets (one per template), found {len(ec2_fleet_ids)}" ) # Step 5: Terminate all instances from both EC2 Fleets at once @@ -564,7 +637,7 @@ def test_multi_ec2_fleet_termination(setup_multi_ec2_fleet_templates): # Check if instances are detached from EC2 Fleets if not termination_started: - detached = verify_ec2_fleet_instances_detached(all_instance_ids) + detached = verify_ec2_fleet_instances_detached(all_instance_ids, list(ec2_fleet_ids)) if detached: log.info("All instances successfully detached from EC2 Fleets") termination_started = True @@ -587,7 +660,7 @@ def test_multi_ec2_fleet_termination(setup_multi_ec2_fleet_templates): assert final_terminating, "Some instances are not in terminating/terminated state" # Verify instances are detached from EC2 Fleets - final_detached = verify_ec2_fleet_instances_detached(all_instance_ids) + final_detached = verify_ec2_fleet_instances_detached(all_instance_ids, list(ec2_fleet_ids)) if not final_detached: log.info( "Note: Some instances still show EC2 Fleet attachment while terminating - this is expected AWS behavior" @@ -600,7 +673,7 @@ def test_multi_ec2_fleet_termination(setup_multi_ec2_fleet_templates): def check_ec2_fleet_exists(fleet_id: str) -> bool: """Check if an EC2 Fleet still exists and is active.""" try: - response = ec2_client.describe_fleets(FleetIds=[fleet_id]) + response = _get_ec2_client().describe_fleets(FleetIds=[fleet_id]) if response.get("Fleets"): fleet_state = response["Fleets"][0].get("FleetState") return fleet_state in ["active", "modifying"] @@ -624,7 +697,7 @@ def check_ec2_fleet_exists(fleet_id: str) -> bool: if check_ec2_fleet_exists(fleet_id): # Log current fleet status for debugging try: - response = ec2_client.describe_fleets(FleetIds=[fleet_id]) + response = _get_ec2_client().describe_fleets(FleetIds=[fleet_id]) if response.get("Fleets"): fleet = response["Fleets"][0] fleet_state = fleet.get("FleetState", "unknown") @@ -663,7 +736,7 @@ def check_ec2_fleet_exists(fleet_id: str) -> bool: # Check if this is a maintain fleet that should have been deleted try: - response = ec2_client.describe_fleets(FleetIds=[fleet_id]) + response = _get_ec2_client().describe_fleets(FleetIds=[fleet_id]) if response.get("Fleets"): fleet = response["Fleets"][0] fleet_type = fleet.get("Type", "unknown") @@ -676,7 +749,7 @@ def check_ec2_fleet_exists(fleet_id: str) -> bool: # Log detailed information about remaining fleets for debugging for fleet_id in final_remaining_fleets: try: - response = ec2_client.describe_fleets(FleetIds=[fleet_id]) + response = _get_ec2_client().describe_fleets(FleetIds=[fleet_id]) if response.get("Fleets"): fleet = response["Fleets"][0] log.info(f"EC2 Fleet {fleet_id} still exists after termination:") diff --git a/tests/onaws/test_multi_resource_termination.py b/tests/providers/aws/live/test_multi_resource_termination.py similarity index 87% rename from tests/onaws/test_multi_resource_termination.py rename to tests/providers/aws/live/test_multi_resource_termination.py index 8129358da..049a19b2b 100644 --- a/tests/onaws/test_multi_resource_termination.py +++ b/tests/providers/aws/live/test_multi_resource_termination.py @@ -1,6 +1,7 @@ import json import logging import os +import re import time import uuid from pathlib import Path @@ -12,11 +13,11 @@ from jsonschema import ValidationError, validate as validate_json_schema from hfmock import HostFactoryMock -from tests.onaws import plugin_io_schemas -from tests.onaws.cleanup_helpers import cleanup_tracked_requests -from tests.onaws.parse_output import parse_and_print_output -from tests.onaws.template_processor import TemplateProcessor -from tests.onaws.test_onaws import get_instances_states +from tests.providers.aws.live import plugin_io_schemas +from tests.providers.aws.live.cleanup_helpers import cleanup_tracked_requests +from tests.providers.aws.live.parse_output import parse_and_print_output +from tests.providers.aws.live.template_processor import TemplateProcessor +from tests.providers.aws.live.test_onaws import get_instances_states pytestmark = [ # Apply default markers to every test in this module pytest.mark.manual_aws, @@ -31,15 +32,61 @@ os.environ["AWS_PROVIDER_LOG_DIR"] = "./logs" os.environ["LOG_DESTINATION"] = "file" -_boto_session = boto3.Session() -_ec2_region = ( - os.environ.get("AWS_REGION") - or os.environ.get("AWS_DEFAULT_REGION") - or _boto_session.region_name - or "eu-west-1" -) -ec2_client = _boto_session.client("ec2", region_name=_ec2_region) -autoscaling_client = _boto_session.client("autoscaling", region_name=_ec2_region) +_ec2_client = None +_autoscaling_client = None + + +def _get_boto_profile_and_region() -> tuple[str | None, str]: + """Read AWS profile and region from ORB_CONFIG_DIR config, same as test_onaws._get_boto_clients.""" + import json as _json + + profile = None + region = None + config_dir = os.environ.get("ORB_CONFIG_DIR") + if config_dir: + try: + config_path = os.path.join(config_dir, "config.json") + with open(config_path) as _f: + config = _json.load(_f) + providers = config.get("provider", {}).get("providers", []) + if providers: + provider_cfg = providers[0].get("config", {}) + profile = provider_cfg.get("profile") + region = provider_cfg.get("region") + except Exception: + pass + # Fall back to AWS_PROFILE env var so the explicit profile_name is always + # non-None when a profile is available. This forces botocore to use + # profile-based credentials and ignore injected fake env var credentials. + profile = profile or os.environ.get("AWS_PROFILE") + region = ( + region + or os.environ.get("AWS_REGION") + or os.environ.get("AWS_DEFAULT_REGION") + or "eu-west-1" + ) + return profile, region + + +def _get_ec2_client(): + global _ec2_client + if _ec2_client is None: + _profile, _region = _get_boto_profile_and_region() + _ec2_client = boto3.Session(profile_name=_profile, region_name=_region).client( + "ec2", region_name=_region + ) + return _ec2_client + + +def _get_autoscaling_client(): + global _autoscaling_client + if _autoscaling_client is None: + _profile, _region = _get_boto_profile_and_region() + _autoscaling_client = boto3.Session(profile_name=_profile, region_name=_region).client( + "autoscaling", region_name=_region + ) + return _autoscaling_client + log = logging.getLogger("multi_resource_test") log.setLevel(logging.DEBUG) @@ -52,12 +99,13 @@ log.addHandler(console_handler) MAX_TIME_WAIT_FOR_CAPACITY_PROVISIONING_SEC = 180 +_IPV4_RE = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$") def check_all_instances_terminating_or_terminated(instance_ids: List[str]) -> bool: """Check if all instances are in terminating or terminated state.""" all_terminating = True - instance_states = get_instances_states(instance_ids, ec2_client) + instance_states = get_instances_states(instance_ids, _get_ec2_client()) for instance_id, state in zip(instance_ids, instance_states): if state is not None: @@ -77,7 +125,7 @@ def categorize_instances_by_resource_type(instance_ids: List[str]) -> Dict[str, categorized = {"ASG": [], "EC2Fleet": [], "SpotFleet": [], "RunInstances": []} try: - response = ec2_client.describe_instances(InstanceIds=instance_ids) + response = _get_ec2_client().describe_instances(InstanceIds=instance_ids) for reservation in response.get("Reservations", []): for instance in reservation.get("Instances", []): @@ -108,7 +156,7 @@ def categorize_instances_by_resource_type(instance_ids: List[str]) -> Dict[str, potential_asg_instances = categorized["RunInstances"].copy() if potential_asg_instances: try: - asg_response = autoscaling_client.describe_auto_scaling_instances( + asg_response = _get_autoscaling_client().describe_auto_scaling_instances( InstanceIds=potential_asg_instances ) @@ -191,7 +239,9 @@ def setup_multi_resource_templates(test_session_id): processor.generate_combined_templates(test_name, template_configs) # Set environment variables - os.environ["HF_PROVIDER_CONFDIR"] = str(test_config_dir) + config_dir = test_config_dir / "config" + os.environ["ORB_CONFIG_DIR"] = str(config_dir) + os.environ["HF_PROVIDER_CONFDIR"] = str(config_dir) os.environ["HF_PROVIDER_LOGDIR"] = str(test_config_dir / "logs") os.environ["HF_PROVIDER_WORKDIR"] = str(test_config_dir / "work") os.environ["AWS_PROVIDER_LOG_DIR"] = str(test_config_dir / "logs") @@ -217,10 +267,19 @@ def _tracking_request_machines(template_name: str, machine_count: int): yield hfm, template_configs try: - cleanup_tracked_requests(_tracked_request_ids, hfm, ec2_client) + cleanup_tracked_requests(_tracked_request_ids, hfm, _get_ec2_client()) except Exception as exc: log.warning("Fixture teardown: cleanup_tracked_requests failed: %s", exc) + for key in ( + "ORB_CONFIG_DIR", + "HF_PROVIDER_CONFDIR", + "HF_PROVIDER_LOGDIR", + "HF_PROVIDER_WORKDIR", + "AWS_PROVIDER_LOG_DIR", + "HF_LOGDIR", + ): + os.environ.pop(key, None) processor.cleanup_test_templates(test_name) log.removeHandler(file_handler) file_handler.close() @@ -301,7 +360,7 @@ def provision_resource_capacity( # dead code - commented out # machines = status_response["requests"][0]["machines"] # # instance_ids = [machine.get("machineId") or machine.get("machine_id") for machine in machines] - # instance_states = get_instances_states(instance_ids, ec2_client) + # instance_states = get_instances_states(instance_ids, _get_ec2_client()) # # for machine, state in zip(machines, instance_states): # assert machine["status"] in ["running", "pending"] @@ -509,11 +568,17 @@ def test_multi_resource_termination(setup_multi_resource_templates): instance_ids = [ machine.get("machineId") or machine.get("machine_id") for machine in machines ] - instance_states = get_instances_states(instance_ids, ec2_client) + instance_states = get_instances_states(instance_ids, _get_ec2_client()) for machine, state in zip(machines, instance_states): assert machine["status"] in ["running", "pending"] instance_id = machine.get("machineId") or machine.get("machine_id") + if machine["status"] == "running": + ip = machine.get("privateIpAddress") or machine.get("private_ip_address") + assert ip, f"Running machine {instance_id} has empty privateIpAddress" + assert _IPV4_RE.match(ip), ( + f"Running machine {instance_id} has invalid privateIpAddress: {ip!r}" + ) assert state is not None assert state in ["running", "pending"] log.debug(f"EC2 {instance_id} state: {state}") @@ -590,7 +655,7 @@ def test_multi_resource_termination(setup_multi_resource_templates): current_categorization = categorize_instances_by_resource_type(all_instance_ids) for resource_type, instances in current_categorization.items(): if instances: - instance_states = get_instances_states(instances, ec2_client) + instance_states = get_instances_states(instances, _get_ec2_client()) terminating_count = sum( 1 for state in instance_states if state in ["shutting-down", "terminated"] ) @@ -646,7 +711,7 @@ def test_multi_resource_termination(setup_multi_resource_templates): for resource_type, instances in final_categorization.items(): if instances: log.info(f" {resource_type}: {len(instances)} instances") - instance_states = get_instances_states(instances, ec2_client) + instance_states = get_instances_states(instances, _get_ec2_client()) for instance_id, state in zip(instances, instance_states): log.info(f" {instance_id}: {state if state is not None else 'terminated'}") diff --git a/tests/onaws/test_multi_spot_fleet_termination.py b/tests/providers/aws/live/test_multi_spot_fleet_termination.py similarity index 87% rename from tests/onaws/test_multi_spot_fleet_termination.py rename to tests/providers/aws/live/test_multi_spot_fleet_termination.py index a5952e606..9bf840167 100644 --- a/tests/onaws/test_multi_spot_fleet_termination.py +++ b/tests/providers/aws/live/test_multi_spot_fleet_termination.py @@ -1,6 +1,7 @@ import json import logging import os +import re import time import uuid from pathlib import Path @@ -12,11 +13,11 @@ from jsonschema import ValidationError, validate as validate_json_schema from hfmock import HostFactoryMock -from tests.onaws import plugin_io_schemas -from tests.onaws.cleanup_helpers import cleanup_tracked_requests -from tests.onaws.parse_output import parse_and_print_output -from tests.onaws.template_processor import TemplateProcessor -from tests.onaws.test_onaws import get_instances_states +from tests.providers.aws.live import plugin_io_schemas +from tests.providers.aws.live.cleanup_helpers import cleanup_tracked_requests +from tests.providers.aws.live.parse_output import parse_and_print_output +from tests.providers.aws.live.template_processor import TemplateProcessor +from tests.providers.aws.live.test_onaws import get_instances_states pytestmark = [ # Apply default markers to every test in this module pytest.mark.manual_aws, @@ -31,14 +32,50 @@ os.environ["AWS_PROVIDER_LOG_DIR"] = "./logs" os.environ["LOG_DESTINATION"] = "file" -_boto_session = boto3.Session() -_ec2_region = ( - os.environ.get("AWS_REGION") - or os.environ.get("AWS_DEFAULT_REGION") - or _boto_session.region_name - or "eu-west-1" -) -ec2_client = _boto_session.client("ec2", region_name=_ec2_region) +_ec2_client = None + + +def _get_boto_profile_and_region() -> tuple[str | None, str]: + """Read AWS profile and region from ORB_CONFIG_DIR config.""" + import json as _json + + profile = None + region = None + config_dir = os.environ.get("ORB_CONFIG_DIR") + if config_dir: + try: + config_path = os.path.join(config_dir, "config.json") + with open(config_path) as _f: + config = _json.load(_f) + providers = config.get("provider", {}).get("providers", []) + if providers: + provider_cfg = providers[0].get("config", {}) + profile = provider_cfg.get("profile") + region = provider_cfg.get("region") + except Exception: + pass + # Fall back to AWS_PROFILE env var so the explicit profile_name is always + # non-None when a profile is available. This forces botocore to use + # profile-based credentials and ignore injected fake env var credentials. + profile = profile or os.environ.get("AWS_PROFILE") + region = ( + region + or os.environ.get("AWS_REGION") + or os.environ.get("AWS_DEFAULT_REGION") + or "eu-west-1" + ) + return profile, region + + +def _get_ec2_client(): + global _ec2_client + if _ec2_client is None: + _profile, _region = _get_boto_profile_and_region() + _ec2_client = boto3.Session(profile_name=_profile, region_name=_region).client( + "ec2", region_name=_region + ) + return _ec2_client + log = logging.getLogger("multi_spot_fleet_test") log.setLevel(logging.DEBUG) @@ -51,12 +88,13 @@ log.addHandler(console_handler) MAX_TIME_WAIT_FOR_CAPACITY_PROVISIONING_SEC = 300 +_IPV4_RE = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$") def get_spot_fleet_instances(fleet_id: str) -> List[str]: """Get all instance IDs from a Spot Fleet.""" try: - response = ec2_client.describe_spot_fleet_instances(SpotFleetRequestId=fleet_id) + response = _get_ec2_client().describe_spot_fleet_instances(SpotFleetRequestId=fleet_id) return [instance["InstanceId"] for instance in response.get("ActiveInstances", [])] except ClientError as e: log.error(f"Error getting Spot Fleet instances for {fleet_id}: {e}") @@ -70,7 +108,7 @@ def verify_spot_fleet_instances_detached(instance_ids: List[str]) -> bool: return True # Get all spot fleet requests (no state filter parameter exists in AWS API) - response = ec2_client.describe_spot_fleet_requests() + response = _get_ec2_client().describe_spot_fleet_requests() # Check each fleet for our instances, but only active/modifying fleets for fleet in response.get("SpotFleetRequestConfigs", []): @@ -81,7 +119,7 @@ def verify_spot_fleet_instances_detached(instance_ids: List[str]) -> bool: continue try: - fleet_instances = ec2_client.describe_spot_fleet_instances( + fleet_instances = _get_ec2_client().describe_spot_fleet_instances( SpotFleetRequestId=fleet_id ) active_instance_ids = [ @@ -116,7 +154,7 @@ def verify_spot_fleet_instances_detached(instance_ids: List[str]) -> bool: def check_all_instances_terminating_or_terminated(instance_ids: List[str]) -> bool: """Check if all instances are in terminating or terminated state.""" all_terminating = True - instance_states = get_instances_states(instance_ids, ec2_client) + instance_states = get_instances_states(instance_ids, _get_ec2_client()) for instance_id, state in zip(instance_ids, instance_states): if state is not None: @@ -179,7 +217,9 @@ def setup_multi_spot_fleet_templates(test_session_id): processor.generate_combined_templates(test_name, template_configs) # Set environment variables - os.environ["HF_PROVIDER_CONFDIR"] = str(test_config_dir) + config_dir = test_config_dir / "config" + os.environ["ORB_CONFIG_DIR"] = str(config_dir) + os.environ["HF_PROVIDER_CONFDIR"] = str(config_dir) os.environ["HF_PROVIDER_LOGDIR"] = str(test_config_dir / "logs") os.environ["HF_PROVIDER_WORKDIR"] = str(test_config_dir / "work") os.environ["AWS_PROVIDER_LOG_DIR"] = str(test_config_dir / "logs") @@ -205,10 +245,19 @@ def _tracking_request_machines(template_name: str, machine_count: int): yield hfm, template_configs try: - cleanup_tracked_requests(_tracked_request_ids, hfm, ec2_client) + cleanup_tracked_requests(_tracked_request_ids, hfm, _get_ec2_client()) except Exception as exc: log.warning("Fixture teardown: cleanup_tracked_requests failed: %s", exc) + for key in ( + "ORB_CONFIG_DIR", + "HF_PROVIDER_CONFDIR", + "HF_PROVIDER_LOGDIR", + "HF_PROVIDER_WORKDIR", + "AWS_PROVIDER_LOG_DIR", + "HF_LOGDIR", + ): + os.environ.pop(key, None) processor.cleanup_test_templates(test_name) log.removeHandler(file_handler) file_handler.close() @@ -291,7 +340,7 @@ def provision_spot_fleet_capacity( # dead code - commented out # machines = status_response["requests"][0]["machines"] # # instance_ids = [machine.get("machineId") or machine.get("machine_id") for machine in machines] - # instance_states = get_instances_states(instance_ids, ec2_client) + # instance_states = get_instances_states(instance_ids, _get_ec2_client()) # # for machine, state in zip(machines, instance_states): # assert machine["status"] in ["running", "pending"] @@ -488,11 +537,17 @@ def test_multi_spot_fleet_termination(setup_multi_spot_fleet_templates): instance_ids = [ machine.get("machineId") or machine.get("machine_id") for machine in machines ] - instance_states = get_instances_states(instance_ids, ec2_client) + instance_states = get_instances_states(instance_ids, _get_ec2_client()) for machine, state in zip(machines, instance_states): assert machine["status"] in ["running", "pending"] instance_id = machine.get("machineId") or machine.get("machine_id") + if machine["status"] == "running": + ip = machine.get("privateIpAddress") or machine.get("private_ip_address") + assert ip, f"Running machine {instance_id} has empty privateIpAddress" + assert _IPV4_RE.match(ip), ( + f"Running machine {instance_id} has invalid privateIpAddress: {ip!r}" + ) assert state is not None assert state in ["running", "pending"] log.debug(f"EC2 {instance_id} state: {state}") @@ -514,7 +569,7 @@ def test_multi_spot_fleet_termination(setup_multi_spot_fleet_templates): # Get Spot Fleet IDs from the instances by checking tags spot_fleet_ids = set() try: - response = ec2_client.describe_instances(InstanceIds=all_instance_ids) + response = _get_ec2_client().describe_instances(InstanceIds=all_instance_ids) for reservation in response.get("Reservations", []): for instance in reservation.get("Instances", []): @@ -598,7 +653,9 @@ def test_multi_spot_fleet_termination(setup_multi_spot_fleet_templates): log.info("Step 8: Documenting Spot Fleet request state (request-type fleets stay active)") for fleet_id in spot_fleet_ids: try: - response = ec2_client.describe_spot_fleet_requests(SpotFleetRequestIds=[fleet_id]) + response = _get_ec2_client().describe_spot_fleet_requests( + SpotFleetRequestIds=[fleet_id] + ) if response.get("SpotFleetRequestConfigs"): fleet = response["SpotFleetRequestConfigs"][0] state = fleet.get("SpotFleetRequestState", "unknown") diff --git a/tests/onaws/test_onaws.py b/tests/providers/aws/live/test_onaws.py similarity index 90% rename from tests/onaws/test_onaws.py rename to tests/providers/aws/live/test_onaws.py index 10d7135da..b760a8e86 100644 --- a/tests/onaws/test_onaws.py +++ b/tests/providers/aws/live/test_onaws.py @@ -1,6 +1,7 @@ import json import logging import os +import re import time from typing import Optional @@ -10,7 +11,7 @@ from jsonschema import ValidationError, validate as validate_json_schema from hfmock import HostFactoryMock -from tests.onaws import plugin_io_schemas, scenarios +from tests.providers.aws.live import plugin_io_schemas, scenarios # EC2Fleet + maintain combinations are now stable and re-enabled here. # They were previously commented out in scenarios.DEFAULT_ATTRIBUTE_COMBINATIONS. @@ -21,12 +22,12 @@ "scheduler": ["default", "hostfactory"], } scenarios.DEFAULT_ATTRIBUTE_COMBINATIONS.append(_EC2FLEET_MAINTAIN_COMBINATIONS) -from tests.onaws.cleanup_helpers import ( +from tests.providers.aws.live.cleanup_helpers import ( cleanup_launch_templates_for_request, wait_for_instances_terminated, ) -from tests.onaws.parse_output import parse_and_print_output -from tests.onaws.template_processor import TemplateProcessor +from tests.providers.aws.live.parse_output import parse_and_print_output +from tests.providers.aws.live.template_processor import TemplateProcessor pytestmark = [ # Apply default markers to every test in this module pytest.mark.manual_aws, @@ -61,6 +62,12 @@ def _get_boto_clients(): except Exception: pass # Fall back to defaults + # Fall back to AWS_PROFILE so the explicit profile_name is non-None when a + # profile is available. pytest-env injects AWS_ACCESS_KEY_ID=testing for + # unit tests; botocore resolves env-var credentials before profile + # credentials, so any session without an explicit profile_name uses the + # fake creds and gets AuthFailure. An explicit profile_name bypasses them. + profile = profile or os.environ.get("AWS_PROFILE") region = ( region or os.environ.get("AWS_REGION") @@ -388,6 +395,106 @@ def _get_resource_id_from_instance(instance_id: str, provider_api: str) -> Optio return None +def _get_asg_instance_weight(instance_id: str, asg_name: str) -> int: + """Return the WeightedCapacity of an instance within an ASG. + + Looks up the instance type via EC2 describe_instances (works even after the + instance is detached from the ASG), then finds the matching WeightedCapacity + in the ASG's MixedInstancesPolicy overrides. + Returns 1 if the ASG has no per-type weights (uniform fleet). + """ + ec2_client, asg_client = _get_boto_clients() + try: + # Use EC2 describe_instances so we still get the instance type even + # after it has been detached from the ASG. + ec2_resp = ec2_client.describe_instances(InstanceIds=[instance_id]) + reservations = ec2_resp.get("Reservations") or [] + if not reservations: + return 1 + instances = reservations[0].get("Instances") or [] + if not instances: + return 1 + instance_type = instances[0].get("InstanceType") or "" + + asg_resp = asg_client.describe_auto_scaling_groups(AutoScalingGroupNames=[asg_name]) + asgs = asg_resp.get("AutoScalingGroups") or [{}] + asg = asgs[0] + + policy = asg.get("MixedInstancesPolicy") or {} + lt = policy.get("LaunchTemplate") or {} + overrides = lt.get("Overrides") or [] + + for override in overrides: + if override.get("InstanceType") == instance_type: + wc = override.get("WeightedCapacity") + if wc is not None: + return int(wc) + return 1 + except (ClientError, KeyError) as exc: + log.warning("Failed to look up ASG instance weight for %s: %s", instance_id, exc) + return 1 + + +def _get_fleet_instance_weight(instance_id: str, fleet_id: str, provider_api: str) -> int: + """Return the WeightedCapacity of an instance in an EC2Fleet or SpotFleet. + + Reads the instance type via EC2 ``describe_instances`` (works even after + the instance is detached / terminated by the fleet), then looks up the + matching ``WeightedCapacity`` in the fleet's launch-template overrides. + Falls back to 1 if the fleet has no per-type weights. + + ``describe_fleet_instances`` / ``describe_spot_fleet_instances`` only list + *currently active* instances, so they return nothing for instances that + have already been returned — the older implementation always saw an empty + list at this point and silently fell back to 1. + """ + ec2_client, _ = _get_boto_clients() + try: + ec2_resp = ec2_client.describe_instances(InstanceIds=[instance_id]) + reservations = ec2_resp.get("Reservations") or [] + if not reservations: + return 1 + instances = reservations[0].get("Instances") or [] + if not instances: + return 1 + instance_type = instances[0].get("InstanceType") or "" + + is_spot = "spotfleet" in provider_api.lower() or fleet_id.startswith("sfr-") + if is_spot: + resp = ec2_client.describe_spot_fleet_requests(SpotFleetRequestIds=[fleet_id]) + configs = resp.get("SpotFleetRequestConfigs") or [{}] + sfr = configs[0].get("SpotFleetRequestConfig") or {} + for spec in sfr.get("LaunchSpecifications") or []: + if spec.get("InstanceType") == instance_type: + wc = spec.get("WeightedCapacity") + if wc is not None: + return int(float(wc)) + for tpl in sfr.get("LaunchTemplateConfigs") or []: + for override in tpl.get("Overrides") or []: + if override.get("InstanceType") == instance_type: + wc = override.get("WeightedCapacity") + if wc is not None: + return int(float(wc)) + else: + resp = ec2_client.describe_fleets(FleetIds=[fleet_id]) + fleets = resp.get("Fleets") or [{}] + for ltc in fleets[0].get("LaunchTemplateConfigs") or []: + for override in ltc.get("Overrides") or []: + if override.get("InstanceType") == instance_type: + wc = override.get("WeightedCapacity") + if wc is not None: + return int(float(wc)) + return 1 + except (ClientError, KeyError) as exc: + log.warning( + "Failed to look up fleet instance weight for %s in %s: %s", + instance_id, + fleet_id, + exc, + ) + return 1 + + def _get_capacity(provider_api: str, resource_id: str) -> int: """Return target/desired capacity for fleet or ASG.""" ec2_client, asg_client = _get_boto_clients() @@ -948,7 +1055,7 @@ def setup_host_factory_mock_with_scenario(request, monkeypatch, test_session_id) if "[" in test_name and "]" in test_name: scenario_name = test_name.split("[")[1].split("]")[0] - from tests.onaws import scenarios + from tests.providers.aws.live import scenarios test_case = scenarios.get_test_case_by_name(scenario_name) if scenario_name else {} @@ -1039,11 +1146,21 @@ def _tracking_request_machines(template_name: str, machine_count: int): log.info("--keep-logs set: preserving test directory %s", test_config_dir) +_IPV4_RE = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$") + + def _check_request_machines_response_status(status_response): assert status_response["requests"][0]["status"] == "complete" for machine in status_response["requests"][0]["machines"]: # it is possible that ec2 host is still initialising assert machine["status"] in ["running", "pending"] + if machine["status"] == "running": + machine_id = machine.get("machineId") or machine.get("machine_id") + ip = machine.get("privateIpAddress") or machine.get("private_ip_address") + assert ip, f"Running machine {machine_id} has empty privateIpAddress" + assert _IPV4_RE.match(ip), ( + f"Running machine {machine_id} has invalid privateIpAddress: {ip!r}" + ) def _check_all_ec2_hosts_are_being_provisioned(status_response): @@ -1204,9 +1321,9 @@ def _verify_all_resources_cleaned( instances_cleaned = False if instances_cleaned: - log.info("✅ All instances are terminated or terminating") + log.info("All instances are terminated or terminating") else: - log.error("❌ Some instances are still running") + log.error("Some instances are still running") # Check backing resource resource_cleaned = True @@ -1226,10 +1343,10 @@ def _verify_all_resources_cleaned( log.error("ASG %s still has %d instances", resource_id, len(instances)) resource_cleaned = False else: - log.info("✅ ASG %s has no instances", resource_id) + log.info("ASG %s has no instances", resource_id) except ClientError as e: if e.response["Error"]["Code"] == "ValidationError": - log.info("✅ ASG %s no longer exists", resource_id) + log.info("ASG %s no longer exists", resource_id) else: log.error("Error checking ASG %s: %s", resource_id, e) resource_cleaned = False @@ -1241,18 +1358,16 @@ def _verify_all_resources_cleaned( log.error("Fleet %s still has capacity: %d", resource_id, capacity) resource_cleaned = False else: - log.info("✅ Fleet %s has zero capacity", resource_id) + log.info("Fleet %s has zero capacity", resource_id) except Exception as e: - log.info( - "✅ Fleet %s appears to be deleted or inaccessible: %s", resource_id, e - ) + log.info("Fleet %s appears to be deleted or inaccessible: %s", resource_id, e) except Exception as e: log.warning("Could not verify backing resource cleanup: %s", e) if resource_cleaned: - log.info("✅ Backing resource is properly cleaned") + log.info("Backing resource is properly cleaned") else: - log.error("❌ Backing resource still has active resources") + log.error("Backing resource still has active resources") return instances_cleaned and resource_cleaned @@ -1482,6 +1597,37 @@ def provide_release_control_loop(hfm, template_json, capacity_to_request, test_c _check_all_ec2_hosts_are_being_provisioned(status_response) + # Capacity-unit fulfilment check from provider response fields. + # target_units / fulfilled_units reflect capacity units for weighted fleets + # and instance count for unweighted templates (1 unit == 1 instance). + _req0 = status_response.get("requests", [{}])[0] if isinstance(status_response, dict) else {} + _machine_ids_for_check = [ + m.get("machineId") or m.get("machine_id") + for m in _req0.get("machines", []) + if m.get("machineId") or m.get("machine_id") + ] + _target_units = ( + _req0["target_units"] if _req0.get("target_units") is not None else capacity_to_request + ) + _fulfilled_units = ( + _req0["fulfilled_units"] + if _req0.get("fulfilled_units") is not None + else len(_machine_ids_for_check) + ) + assert _fulfilled_units >= _target_units, ( + f"Fleet not fully fulfilled: fulfilled={_fulfilled_units}, target={_target_units}" + ) + # Template-aware instance count sanity check. + if test_case and scenarios.template_uses_weighted_capacity(test_case): + assert len(_machine_ids_for_check) >= 1, ( + f"Expected at least 1 machine (weighted template), got: {_machine_ids_for_check}" + ) + else: + assert len(_machine_ids_for_check) == capacity_to_request, ( + f"Expected {capacity_to_request} machines (unweighted template), " + f"got {len(_machine_ids_for_check)}: {_machine_ids_for_check}" + ) + # Validate instance attributes against template log.info("Starting instance attribute validation against template") attribute_validation_passed = validate_random_instance_attributes( @@ -1611,14 +1757,14 @@ def provide_release_control_loop(hfm, template_json, capacity_to_request, test_c cleanup_verified = _verify_all_resources_cleaned(ec2_instance_ids, resource_id, provider_api) if not cleanup_verified: - log.error("⚠️ Cleanup verification failed - some resources may still exist") + log.error("Cleanup verification failed - some resources may still exist") # Log remaining resources for debugging for instance_id in ec2_instance_ids: state_info = get_instance_state(instance_id) if state_info["exists"]: log.error("Instance %s still exists in state: %s", instance_id, state_info["state"]) else: - log.info("✅ All resources successfully cleaned up") + log.info("All resources successfully cleaned up") @pytest.mark.aws @@ -1813,7 +1959,10 @@ def test_partial_return_reduces_capacity(setup_host_factory_mock_with_scenario, log.info("1.6: Extracting provisioned instance information") machines = status_response["requests"][0]["machines"] machine_ids = [m.get("machineId") or m.get("machine_id") for m in machines] - assert len(machine_ids) >= 2, "Partial return test requires capacity > 1" + if len(machine_ids) < 2: + pytest.skip( + f"Only {len(machine_ids)} physical instance(s) provisioned (weighted capacity scenario) — need 2+ for partial return test" + ) log.info("Provisioned %d instances: %s", len(machine_ids), machine_ids) # === STEP 2: PARTIAL RETURN AND CAPACITY VERIFICATION === @@ -1863,7 +2012,29 @@ def test_partial_return_reduces_capacity(setup_host_factory_mock_with_scenario, # 2.6: Verify capacity reduction log.info("2.6: Verifying capacity reduction") - expected_capacity = max(capacity_before - 1, 0) + # For ASGs with weighted instance types, returning one instance reduces + # DesiredCapacity by the instance's WeightedCapacity (e.g. 2 for t3.medium + # with weight=2), not just by 1. Look up the actual weight so we assert the + # correct expected value regardless of which instance type AWS placed. + if provider_api == "ASG" or "asg" in provider_api.lower(): + capacity_decrement = _get_asg_instance_weight(first_instance, resource_id) + log.info( + "ASG weighted capacity: instance %s has weight %d in ASG %s", + first_instance, + capacity_decrement, + resource_id, + ) + elif provider_api in ("EC2Fleet", "SpotFleet") or resource_id.startswith(("fleet-", "sfr-")): + capacity_decrement = _get_fleet_instance_weight(first_instance, resource_id, provider_api) + log.info( + "Fleet weighted capacity: instance %s has weight %d in fleet %s", + first_instance, + capacity_decrement, + resource_id, + ) + else: + capacity_decrement = 1 + expected_capacity = max(capacity_before - capacity_decrement, 0) # ASG operations can take longer than fleet operations timeout = 120 if provider_api == "ASG" or "asg" in provider_api.lower() else 60 log.info("Waiting for capacity change with timeout=%ds", timeout) @@ -1944,7 +2115,7 @@ def test_partial_return_reduces_capacity(setup_host_factory_mock_with_scenario, cleanup_verified = _verify_all_resources_cleaned(remaining_ids, resource_id, provider_api) if not cleanup_verified: - log.error("⚠️ Cleanup verification failed - some resources may still exist") + log.error("Cleanup verification failed - some resources may still exist") # Log remaining resources for debugging for instance_id in remaining_ids: state_info = get_instance_state(instance_id) @@ -1953,7 +2124,7 @@ def test_partial_return_reduces_capacity(setup_host_factory_mock_with_scenario, "Instance %s still exists in state: %s", instance_id, state_info["state"] ) else: - log.info("✅ All resources successfully cleaned up") + log.info("All resources successfully cleaned up") else: log.info("3.1: No remaining instances to clean up") diff --git a/tests/onaws/test_rest_api_onaws.py b/tests/providers/aws/live/test_rest_api_onaws.py similarity index 85% rename from tests/onaws/test_rest_api_onaws.py rename to tests/providers/aws/live/test_rest_api_onaws.py index 4027ce38c..5aa04b08d 100644 --- a/tests/onaws/test_rest_api_onaws.py +++ b/tests/providers/aws/live/test_rest_api_onaws.py @@ -16,13 +16,13 @@ import requests from botocore.exceptions import ClientError -from tests.onaws import scenarios_rest_api -from tests.onaws.scenarios_rest_api import CUSTOM_TEST_CASES -from tests.onaws.template_processor import TemplateProcessor +from tests.providers.aws.live import scenarios_rest_api +from tests.providers.aws.live.scenarios_rest_api import CUSTOM_TEST_CASES +from tests.providers.aws.live.template_processor import TemplateProcessor # Import AWS validation functions from test_onaws (guarded to allow skip on import failures) try: - from tests.onaws.test_onaws import ( + from tests.providers.aws.live.test_onaws import ( _check_all_ec2_hosts_are_being_terminated, _cleanup_asg_resources, _get_capacity, @@ -57,19 +57,59 @@ # Force region to eu-west-1 for tests os.environ.setdefault("AWS_REGION", "eu-west-1") -# AWS client setup -_boto_session = boto3.Session() -_ec2_region = ( - os.environ.get("AWS_REGION") - or os.environ.get("AWS_DEFAULT_REGION") - or _boto_session.region_name - or "eu-west-1" -) -ec2_client = _boto_session.client("ec2", region_name=_ec2_region) -asg_client = _boto_session.client("autoscaling", region_name=_ec2_region) +# AWS client setup — lazy-init so the client is constructed after +# pytest_sessionstart has exported AWS_PROFILE from the ORB config. +_ec2_client = None +_asg_client = None + + +def _get_boto_profile_and_region() -> tuple[str | None, str]: + """Read AWS profile and region from ORB_CONFIG_DIR config.""" + import json as _json + + profile = None + region = None + config_dir = os.environ.get("ORB_CONFIG_DIR") + if config_dir: + try: + config_path = os.path.join(config_dir, "config.json") + with open(config_path) as _f: + config = _json.load(_f) + providers = config.get("provider", {}).get("providers", []) + if providers: + provider_cfg = providers[0].get("config", {}) + profile = provider_cfg.get("profile") + region = provider_cfg.get("region") + except Exception: + pass + region = ( + region + or os.environ.get("AWS_REGION") + or os.environ.get("AWS_DEFAULT_REGION") + or "eu-west-1" + ) + return profile, region + + +def _get_ec2_client(): + global _ec2_client + if _ec2_client is None: + _profile, _region = _get_boto_profile_and_region() + _ec2_client = boto3.Session(profile_name=_profile, region_name=_region).client( + "ec2", region_name=_region + ) + return _ec2_client + + +def _get_asg_client(): + global _asg_client + if _asg_client is None: + _profile, _region = _get_boto_profile_and_region() + _asg_client = boto3.Session(profile_name=_profile, region_name=_region).client( + "autoscaling", region_name=_region + ) + return _asg_client -# Log the configured region for debugging -print(f"AWS clients initialized with region: {_ec2_region}") # Logger setup log = logging.getLogger("rest_api_test") @@ -109,36 +149,60 @@ def __init__(self, message: str, status_response: dict | None = None): self.status_response = status_response +def _pick_free_port() -> int: + """Bind to port 0 and immediately release it to claim an OS-assigned ephemeral port. + + Avoids hard-coded ports colliding across pytest-xdist workers running in parallel. + There is a small TOCTOU race between releasing and the server binding — acceptable + for test isolation; the kernel does not reuse a port immediately under normal load. + """ + import socket + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + class ORBServerManager: """Manage ORB server lifecycle for testing.""" def __init__( self, host: str = scenarios_rest_api.REST_API_SERVER["host"], - port: int = scenarios_rest_api.REST_API_SERVER["port"], + port: Optional[int] = None, log_path: Optional[str] = None, ): self.host = host - self.port = port + # Auto-pick a free port per instance unless explicitly overridden. + # Configured `port` in scenarios is only honoured when explicitly passed + # (e.g. a single-worker run that wants to hit a known port). + self.port = port if port is not None else _pick_free_port() self.process = None - self.base_url = f"http://{host}:{port}" + # Health probe must target localhost (not 0.0.0.0 or wildcard host). + self.base_url = f"http://127.0.0.1:{self.port}" self.log_path = log_path self._log_file_handle = None + self._captured_log_path: Optional[str] = None def start(self, timeout: int | None = None): - """Start ORB server: orb system serve --host 0.0.0.0 --port 8000""" + """Start ORB server: orb system serve --host --port """ cmd = ["orb", "system", "serve", "--host", self.host, "--port", str(self.port)] - log.info(f"Starting ORB server: {' '.join(cmd)}") + log.info("Starting ORB server: %s", " ".join(cmd)) - stdout_target = subprocess.PIPE - stderr_target = subprocess.PIPE + # Always capture server output to a file so a failed start has actionable detail. + if not self.log_path: + # Fall back to a temp file the test can surface in the error message. + import tempfile - # If a log path is provided, write combined stdout/stderr to that file - if self.log_path: + fd, self.log_path = tempfile.mkstemp(prefix=f"orb-server-{self.port}-", suffix=".log") + os.close(fd) + else: os.makedirs(os.path.dirname(self.log_path), exist_ok=True) - self._log_file_handle = open(self.log_path, "w", encoding="utf-8") - stdout_target = self._log_file_handle - stderr_target = subprocess.STDOUT + + self._captured_log_path = self.log_path + self._log_file_handle = open(self.log_path, "w", encoding="utf-8") + stdout_target = self._log_file_handle + stderr_target = subprocess.STDOUT if timeout is None: timeout = REST_TIMEOUTS["server_start"] @@ -150,31 +214,50 @@ def start(self, timeout: int | None = None): text=True, ) - # Wait for server to be ready + # Wait for server to be ready, AND fail fast if the process dies. start_time = time.time() while time.time() - start_time < timeout: + # If the process has exited, no point continuing to poll. + if self.process.poll() is not None: + break try: response = requests.get( f"{self.base_url}/health", timeout=scenarios_rest_api.REST_API_SERVER["start_probe_timeout"], ) if response.status_code == 200: - log.info(f"ORB server started successfully on {self.base_url}") + log.info("ORB server started successfully on %s", self.base_url) return except requests.exceptions.RequestException: time.sleep(scenarios_rest_api.REST_API_SERVER["start_probe_interval"]) - # Server failed to start - capture output - try: - _stdout, stderr = self.process.communicate( - timeout=scenarios_rest_api.REST_API_SERVER["start_capture_timeout"] - ) - error_msg = f"ORB server failed to start within {timeout}s. stderr: {stderr}" - except subprocess.TimeoutExpired: - error_msg = f"ORB server failed to start within {timeout}s (process still running)" - + # Server failed to start — read captured output from the log file. + # The subprocess.PIPE/communicate dance doesn't work here because we + # already redirected output to the log file. + captured = self._read_captured_output() + exit_code = self.process.poll() if self.process else None + error_msg = ( + f"ORB server failed to start within {timeout}s on {self.base_url} " + f"(exit_code={exit_code}, log={self._captured_log_path}).\n" + f"--- captured output (last 2KB) ---\n{captured}" + ) raise RuntimeError(error_msg) + def _read_captured_output(self) -> str: + """Read the last 2KB of the server log for error context.""" + try: + if self._log_file_handle: + self._log_file_handle.flush() + if not self._captured_log_path or not os.path.exists(self._captured_log_path): + return "(no log file)" + size = os.path.getsize(self._captured_log_path) + with open(self._captured_log_path, encoding="utf-8", errors="replace") as f: + if size > 2048: + f.seek(size - 2048) + return f.read() + except Exception as exc: # noqa: BLE001 + return f"(failed to read log: {exc})" + def stop(self): """Terminate ORB server process.""" if self.process: @@ -273,10 +356,10 @@ def return_machines(self, machine_ids: List[str]) -> dict: return self._handle_response(response) def get_request_details(self, request_id: str) -> dict: - """GET /api/v1/requests/{request_id}""" - log.debug(f"GET /api/v1/requests/{request_id}") + """GET /api/v1/requests/{request_id}/status — bare /requests/{id} was removed.""" + log.debug(f"GET /api/v1/requests/{request_id}/status") response = self.session.get( - self._url(f"/requests/{request_id}"), + self._url(f"/requests/{request_id}/status"), timeout=self.timeout, ) return self._handle_response(response) @@ -326,7 +409,9 @@ def setup_rest_api_environment(request, test_session_id): ) # Configure environment (must be set before server start) - os.environ["HF_PROVIDER_CONFDIR"] = str(test_config_dir) + config_dir = test_config_dir / "config" + os.environ["ORB_CONFIG_DIR"] = str(config_dir) + os.environ["HF_PROVIDER_CONFDIR"] = str(config_dir) os.environ["HF_PROVIDER_LOGDIR"] = str(test_config_dir / "logs") os.environ["HF_PROVIDER_WORKDIR"] = str(test_config_dir / "work") os.environ["DEFAULT_PROVIDER_WORKDIR"] = str(test_config_dir / "work") @@ -343,6 +428,22 @@ def setup_rest_api_environment(request, test_session_id): yield test_case + for key in ( + "ORB_CONFIG_DIR", + "HF_PROVIDER_CONFDIR", + "HF_PROVIDER_LOGDIR", + "HF_PROVIDER_WORKDIR", + "DEFAULT_PROVIDER_WORKDIR", + "AWS_PROVIDER_LOG_DIR", + "METRICS_DIR", + ): + os.environ.pop(key, None) + try: + from orb.infrastructure.di import reset_container + + reset_container() + except Exception: + pass try: processor.cleanup_test_templates(test_name) except Exception as exc: @@ -387,7 +488,7 @@ def _lookup_aws_capacity_progress(provider_api: str, resource_id: str) -> tuple[ try: if "spotfleet" in provider_lower: - resp = ec2_client.describe_spot_fleet_requests(SpotFleetRequestIds=[resource_id]) + resp = _get_ec2_client().describe_spot_fleet_requests(SpotFleetRequestIds=[resource_id]) configs = resp.get("SpotFleetRequestConfigs") or [] config = configs[0].get("SpotFleetRequestConfig", {}) if configs else {} target = int(config.get("TargetCapacity", 0) or 0) @@ -395,7 +496,7 @@ def _lookup_aws_capacity_progress(provider_api: str, resource_id: str) -> tuple[ return fulfilled, target if "ec2fleet" in provider_lower: - resp = ec2_client.describe_fleets(FleetIds=[resource_id]) + resp = _get_ec2_client().describe_fleets(FleetIds=[resource_id]) fleets = resp.get("Fleets") or [] fleet = fleets[0] if fleets else {} target_spec = fleet.get("TargetCapacitySpecification", {}) or {} @@ -404,7 +505,9 @@ def _lookup_aws_capacity_progress(provider_api: str, resource_id: str) -> tuple[ return fulfilled, target if provider_lower == "asg" or "autoscaling" in provider_lower: - resp = asg_client.describe_auto_scaling_groups(AutoScalingGroupNames=[resource_id]) + resp = _get_asg_client().describe_auto_scaling_groups( + AutoScalingGroupNames=[resource_id] + ) asg = (resp.get("AutoScalingGroups") or [{}])[0] target = int(asg.get("DesiredCapacity", 0) or 0) fulfilled = len(asg.get("Instances") or []) @@ -440,9 +543,9 @@ def _log_aws_capacity_progress( machines = first_request.get("machines") or [] machine_ids = [ - machine.get("machine_id") + machine.get("machineId") or machine.get("machine_id") for machine in machines - if isinstance(machine, dict) and machine.get("machine_id") + if isinstance(machine, dict) and (machine.get("machineId") or machine.get("machine_id")) ] if not machine_ids: @@ -550,13 +653,10 @@ def log_resource_history(resource_id: str, provider_api: str) -> None: next_token = None # Add 60 second buffer to account for clock skew between client and AWS buffered_start_time = start_time - timedelta(seconds=3600) - # Set end time 5 hours in the future to ensure we get all events - end_time = current_time + timedelta(hours=5) while api_call_count < config["max_api_calls"]: params = { "FleetId": resource_id, "StartTime": buffered_start_time, - "EndTime": end_time, # Use calculated end time for bounded queries "MaxResults": config["max_records_per_call"], } if next_token: @@ -567,7 +667,7 @@ def log_resource_history(resource_id: str, provider_api: str) -> None: f"EC2Fleet history API call {api_call_count}/{config['max_api_calls']}, requesting up to {params['MaxResults']} records" ) - response = ec2_client.describe_fleet_history(**params) + response = _get_ec2_client().describe_fleet_history(**params) all_records = response.get("HistoryRecords", []) # Filter records to exclude already-seen records @@ -612,7 +712,7 @@ def log_resource_history(resource_id: str, provider_api: str) -> None: api_call_count += 1 log.debug(f"SpotFleet history API call {api_call_count}/{config['max_api_calls']}") - response = ec2_client.describe_spot_fleet_request_history(**params) + response = _get_ec2_client().describe_spot_fleet_request_history(**params) all_records = response.get("HistoryRecords", []) # Filter records to exclude already-seen records @@ -641,7 +741,7 @@ def log_resource_history(resource_id: str, provider_api: str) -> None: elif provider_api == "ASG": log.debug( - f"Fetching ASG scaling activities for {resource_id} (region={asg_client.meta.region_name})" + f"Fetching ASG scaling activities for {resource_id} (region={_get_asg_client().meta.region_name})" ) # ASG has a max limit of 100 records per call, so we need to paginate next_token = None @@ -657,7 +757,7 @@ def log_resource_history(resource_id: str, provider_api: str) -> None: api_call_count += 1 # log.debug(f"ASG history API call {api_call_count}, requesting up to {params['MaxRecords']} records") - response = asg_client.describe_scaling_activities(**params) + response = _get_asg_client().describe_scaling_activities(**params) all_activities = response.get("Activities", []) # log.debug(f"Retrieved {len(all_activities)} ASG activities from API") @@ -906,12 +1006,26 @@ def _wait_for_return_completion_rest( status_response = client.get_request_details(return_request_id) log.debug(f"Return request status: {json.dumps(status_response, indent=2)}") - # Check if request is complete - if status_response.get("status") == "complete": + # /requests/{id}/status wraps results in a "requests" list + req_list = status_response.get("requests", []) + req_status = req_list[0].get("status") if req_list else status_response.get("status") + if req_status == "complete": log.info(f"Return request {return_request_id} completed") return status_response except requests.HTTPError as e: - log.debug(f"Error checking return status: {e}") + status_code = e.response.status_code if e.response is not None else None + log.debug(f"Error checking return status (HTTP {status_code}): {e}") + # Non-retryable client errors (not 429 Too Many Requests or 503 Service Unavailable) + if ( + status_code is not None + and status_code not in (429, 503) + and 400 <= status_code < 500 + ): + log.warning( + f"Non-retryable HTTP {status_code} for return request {return_request_id}; " + "aborting poll" + ) + return {} if time.time() - start_time > timeout: log.warning(f"Return request {return_request_id} did not complete within {timeout}s") @@ -953,11 +1067,15 @@ def test_rest_api_partial_return_reduces_capacity( # Step 1: Request capacity templates_response = rest_api_client.get_templates() template_id = test_case.get("template_id") or test_case["test_name"] + scheduler = test_case.get("overrides", {}).get("scheduler", "default") + template_id_key = "templateId" if scheduler == "hostfactory" else "template_id" + provider_api_key = "providerApi" if scheduler == "hostfactory" else "provider_api" + template_json = next( ( template for template in templates_response["templates"] - if template.get("template_id") == template_id + if template.get(template_id_key) == template_id ), None, ) @@ -965,14 +1083,14 @@ def test_rest_api_partial_return_reduces_capacity( pytest.fail(f"Template {template_id} not found for partial return test") provider_api = ( - template_json.get("provider_api") + template_json.get(provider_api_key) or test_case.get("overrides", {}).get("providerApi") or "EC2Fleet" ) log.info("Requesting %d instances", test_case["capacity_to_request"]) request_response = rest_api_client.request_machines( - template_id=template_json["template_id"], + template_id=template_json[template_id_key], machine_count=test_case["capacity_to_request"], ) log.debug("Request response: %s", json.dumps(request_response, indent=2)) @@ -1132,12 +1250,22 @@ def test_00_rest_api_server_health(setup_rest_api_environment): assert down_confirmed, "API should be unreachable after server.stop()" +_IPV4_RE = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$") + + def _check_request_machines_response_status(status_response): """Validate request status response.""" assert status_response["requests"][0]["status"] == "complete" for machine in status_response["requests"][0]["machines"]: # EC2 host may still be initializing assert machine["status"] in ["running", "pending"] + if machine["status"] == "running": + machine_id = machine.get("machineId") or machine.get("machine_id") + ip = machine.get("privateIpAddress") or machine.get("private_ip_address") + assert ip, f"Running machine {machine_id} has empty privateIpAddress" + assert _IPV4_RE.match(ip), ( + f"Running machine {machine_id} has invalid privateIpAddress: {ip!r}" + ) def _describe_instances_bulk(instance_ids: list[str], chunk_size: int = 100) -> dict[str, dict]: @@ -1147,13 +1275,18 @@ def _describe_instances_bulk(instance_ids: list[str], chunk_size: int = 100) -> Returns a mapping of instance_id -> {"exists": bool, "state": str | None}. """ states: dict[str, dict] = {} + # Filter out None values — HostFactory scheduler uses machineId (camelCase) while + # the default scheduler uses machine_id (snake_case). Callers that only try one + # field name will produce a list of Nones when the wrong key is used; silently + # dropping Nones here prevents a botocore.ParamValidationError from crashing the check. + instance_ids = [iid for iid in instance_ids if iid is not None] if not instance_ids: return states for start in range(0, len(instance_ids), chunk_size): chunk = instance_ids[start : start + chunk_size] try: - resp = ec2_client.describe_instances(InstanceIds=chunk) + resp = _get_ec2_client().describe_instances(InstanceIds=chunk) except ClientError as exc: # pragma: no cover - defensive log.warning("describe_instances failed for chunk %s: %s", chunk, exc) continue @@ -1181,18 +1314,28 @@ def _describe_instances_bulk(instance_ids: list[str], chunk_size: int = 100) -> def _check_all_ec2_hosts_are_being_provisioned(status_response): - """Verify all EC2 instances are being provisioned.""" + """Verify all EC2 instances are being provisioned. + + Handles both HostFactory (camelCase ``machineId``) and default-scheduler + (snake_case ``machine_id``) response formats. + """ machines = status_response["requests"][0]["machines"] - instance_ids = [m.get("machine_id") for m in machines] + # Resolve the instance ID regardless of which scheduler produced the response. + # HostFactory uses machineId; the default scheduler uses machine_id. + instance_ids = [m.get("machine_id") or m.get("machineId") for m in machines] states = _describe_instances_bulk(instance_ids) for machine in machines: - ec2_instance_id = machine.get("machine_id") + ec2_instance_id = machine.get("machine_id") or machine.get("machineId") res = states.get(ec2_instance_id, {"exists": False, "state": None}) - assert res["exists"] is True + assert res["exists"] is True, ( + f"Instance {ec2_instance_id!r} not found in AWS describe_instances response" + ) # EC2 host may still be initializing - assert res["state"] in ["running", "pending"] + assert res["state"] in ["running", "pending"], ( + f"Instance {ec2_instance_id!r} is in unexpected state {res['state']!r}" + ) log.debug(f"EC2 {ec2_instance_id} state: {json.dumps(res, indent=4)}") @@ -1276,7 +1419,7 @@ def _fetch_history() -> list[dict]: log.info(f"Calling describe_fleets to determine fleet type for {resource_id}") fleet_desc = None try: - fleet_resp = ec2_client.describe_fleets(FleetIds=[resource_id]) + fleet_resp = _get_ec2_client().describe_fleets(FleetIds=[resource_id]) fleet_desc = (fleet_resp.get("Fleets") or [None])[0] except ClientError as exc: log.warning("describe_fleets failed for %s: %s", resource_id, exc) @@ -1311,7 +1454,9 @@ def _collect_fleet_instance_ids() -> list[str]: # As a last resort, ask AWS directly if not ids: try: - inst_resp = ec2_client.describe_fleet_instances(FleetId=resource_id) + inst_resp = _get_ec2_client().describe_fleet_instances( + FleetId=resource_id + ) for entry in inst_resp.get("ActiveInstances", []) or []: iid = entry.get("InstanceId") if iid: @@ -1340,7 +1485,7 @@ def _collect_fleet_instance_ids() -> list[str]: for start_idx in range(0, len(instance_ids), 100): chunk = instance_ids[start_idx : start_idx + 100] try: - resp = ec2_client.describe_instances(InstanceIds=chunk) + resp = _get_ec2_client().describe_instances(InstanceIds=chunk) except ClientError as exc: log.warning("describe_instances failed for chunk %s: %s", chunk, exc) continue @@ -1389,7 +1534,7 @@ def _collect_fleet_instance_ids() -> list[str]: params["NextToken"] = next_token try: - response = ec2_client.describe_fleet_history(**params) + response = _get_ec2_client().describe_fleet_history(**params) except ClientError as exc: message = str(exc).lower() if ( @@ -1421,7 +1566,7 @@ def _collect_fleet_instance_ids() -> list[str]: if next_token: params["NextToken"] = next_token - response = ec2_client.describe_spot_fleet_request_history(**params) + response = _get_ec2_client().describe_spot_fleet_request_history(**params) history_records.extend(response.get("HistoryRecords", [])) next_token = response.get("NextToken") if not next_token: @@ -1439,7 +1584,7 @@ def _collect_fleet_instance_ids() -> list[str]: if next_token: params["NextToken"] = next_token - response = asg_client.describe_scaling_activities(**params) + response = _get_asg_client().describe_scaling_activities(**params) history_records.extend(response.get("Activities", [])) next_token = response.get("NextToken") if not next_token: @@ -1494,7 +1639,7 @@ def _collect_fleet_instance_ids() -> list[str]: for start in range(0, len(missing_ids), 100): chunk = missing_ids[start : start + 100] try: - resp = ec2_client.describe_instances(InstanceIds=chunk) + resp = _get_ec2_client().describe_instances(InstanceIds=chunk) except ClientError as exc: log.warning("describe_instances failed for chunk %s: %s", chunk, exc) continue @@ -1564,7 +1709,7 @@ def _collect_fleet_instance_ids() -> list[str]: # tooling can compute request_time similarly to fleet history. if provider_api == "ASG": try: - response = asg_client.describe_auto_scaling_groups( + response = _get_asg_client().describe_auto_scaling_groups( AutoScalingGroupNames=[resource_id] ) groups = response.get("AutoScalingGroups", []) @@ -1597,11 +1742,11 @@ def _collect_fleet_instance_ids() -> list[str]: log.info(f"Writing history to file: {history_file}") with open(history_file, "w") as f: json.dump(history_data, f, indent=2, default=str) - log.info(f"✅ Resource history saved successfully to: {history_file}") + log.info(f"Resource history saved successfully to: {history_file}") log.info(f"File size: {os.path.getsize(history_file)} bytes") except Exception as exc: - log.error(f"❌ Failed to capture resource history: {exc}", exc_info=True) + log.error(f"Failed to capture resource history: {exc}", exc_info=True) try: failure_payload = { "resource_id": resource_id, @@ -1620,12 +1765,12 @@ def _terminate_backing_resource(resource_id: str, provider_api: str) -> None: try: if provider_api == "EC2Fleet": log.info(f"Deleting EC2Fleet: {resource_id}") - ec2_client.delete_fleets(FleetIds=[resource_id], TerminateInstances=True) + _get_ec2_client().delete_fleets(FleetIds=[resource_id], TerminateInstances=True) _wait_for_fleet_deletion(resource_id) elif provider_api == "SpotFleet": log.info(f"Cancelling SpotFleet: {resource_id}") - ec2_client.cancel_spot_fleet_requests( + _get_ec2_client().cancel_spot_fleet_requests( SpotFleetRequestIds=[resource_id], TerminateInstances=True ) _wait_for_spot_fleet_deletion(resource_id) @@ -1633,7 +1778,7 @@ def _terminate_backing_resource(resource_id: str, provider_api: str) -> None: elif provider_api == "ASG": log.info(f"Checking Auto Scaling Group status: {resource_id}") try: - response = asg_client.describe_auto_scaling_groups( + response = _get_asg_client().describe_auto_scaling_groups( AutoScalingGroupNames=[resource_id] ) groups = response.get("AutoScalingGroups", []) @@ -1649,7 +1794,7 @@ def _terminate_backing_resource(resource_id: str, provider_api: str) -> None: return log.info(f"Deleting Auto Scaling Group: {resource_id}") - asg_client.delete_auto_scaling_group( + _get_asg_client().delete_auto_scaling_group( AutoScalingGroupName=resource_id, ForceDelete=True ) _wait_for_asg_deletion(resource_id) @@ -1684,7 +1829,7 @@ def _wait_for_fleet_deletion(fleet_id: str, timeout: int = 300) -> None: start = time.time() while time.time() - start < timeout: try: - response = ec2_client.describe_fleets(FleetIds=[fleet_id]) + response = _get_ec2_client().describe_fleets(FleetIds=[fleet_id]) fleets = response.get("Fleets", []) if not fleets or fleets[0]["FleetState"] in ["deleted_terminating", "deleted_running"]: log.info(f"Fleet {fleet_id} is being deleted") @@ -1702,7 +1847,9 @@ def _wait_for_spot_fleet_deletion(fleet_id: str, timeout: int = 300) -> None: start = time.time() while time.time() - start < timeout: try: - response = ec2_client.describe_spot_fleet_requests(SpotFleetRequestIds=[fleet_id]) + response = _get_ec2_client().describe_spot_fleet_requests( + SpotFleetRequestIds=[fleet_id] + ) requests = response.get("SpotFleetRequestConfigs", []) if not requests or requests[0]["SpotFleetRequestState"] in [ "cancelled_terminating", @@ -1723,7 +1870,9 @@ def _wait_for_asg_deletion(asg_name: str, timeout: int = 300) -> None: start = time.time() while time.time() - start < timeout: try: - response = asg_client.describe_auto_scaling_groups(AutoScalingGroupNames=[asg_name]) + response = _get_asg_client().describe_auto_scaling_groups( + AutoScalingGroupNames=[asg_name] + ) groups = response.get("AutoScalingGroups", []) if not groups: log.info(f"ASG {asg_name} deleted") @@ -1783,11 +1932,19 @@ def test_rest_api_control_loop(rest_api_client, setup_rest_api_environment, test # 1.2: Find target template log.info("1.2: Finding target template") template_id = test_case.get("template_id") or test_case["test_name"] + # Each scheduler defines its own wire format. HostFactory uses camelCase + # (IBM Symphony spec); the default scheduler keeps ORB's domain snake_case. + # Tests must match the scheduler under test — otherwise a real wire-format + # regression would be hidden by a permissive lookup. + scheduler = test_case.get("overrides", {}).get("scheduler", "default") + template_id_key = "templateId" if scheduler == "hostfactory" else "template_id" + provider_api_key = "providerApi" if scheduler == "hostfactory" else "provider_api" + template_json = next( ( template for template in templates_response["templates"] - if template.get("template_id") == template_id + if template.get(template_id_key) == template_id ), None, ) @@ -1796,9 +1953,9 @@ def test_rest_api_control_loop(rest_api_client, setup_rest_api_environment, test log.warning(f"Template {template_id} not found, using first available template") template_json = templates_response["templates"][0] - log.info(f"Using template: {template_json.get('template_id')}") + log.info(f"Using template: {template_json.get(template_id_key)}") provider_api = ( - template_json.get("provider_api") + template_json.get(provider_api_key) or test_case.get("overrides", {}).get("providerApi") or "EC2Fleet" ) @@ -1807,14 +1964,15 @@ def test_rest_api_control_loop(rest_api_client, setup_rest_api_environment, test # 1.3: Request machines via REST API log.info(f"1.3: Requesting {test_case['capacity_to_request']} machines") request_response = rest_api_client.request_machines( - template_id=template_json["template_id"], + template_id=template_json[template_id_key], machine_count=test_case["capacity_to_request"], ) log.debug(f"Request response: {json.dumps(request_response, indent=2)}") # 1.4: Validate request response log.info("1.4: Validating request response") - request_id = request_response.get("request_id") + request_id_key = "requestId" if scheduler == "hostfactory" else "request_id" + request_id = request_response.get(request_id_key) if not request_id: pytest.fail(f"Request ID missing in response: {request_response}") assert REQUEST_ID_RE.match(request_id), ( @@ -1861,7 +2019,11 @@ def test_rest_api_control_loop(rest_api_client, setup_rest_api_environment, test requests_list = status_response.get("requests") or [] first_request = requests_list[0] if requests_list else {} machines = first_request.get("machines") or [] - machine_ids_tmp = [m.get("machine_id") for m in machines if m.get("machine_id")] + machine_ids_tmp = [ + m.get("machineId") or m.get("machine_id") + for m in machines + if m.get("machineId") or m.get("machine_id") + ] if machine_ids_tmp: history_resource_id = _get_resource_id_from_instance( machine_ids_tmp[0], provider_api @@ -1919,8 +2081,12 @@ def test_rest_api_control_loop(rest_api_client, setup_rest_api_environment, test if abis_requested: log.info("2.6: Verifying ABIS configuration") first_machine = status_response["requests"][0]["machines"][0] - instance_id = first_machine.get("machine_id") - assert instance_id is not None, "machine_id missing from first machine" + # HostFactory scheduler serialises the instance ID as camelCase "machineId"; + # the default scheduler uses snake_case "machine_id". Try both. + instance_id = first_machine.get("machineId") or first_machine.get("machine_id") + assert instance_id is not None, ( + f"machine_id/machineId missing from first machine: {list(first_machine.keys())}" + ) verify_abis_enabled_for_instance(instance_id) log.info("ABIS verification PASSED") @@ -1929,7 +2095,12 @@ def test_rest_api_control_loop(rest_api_client, setup_rest_api_environment, test # 3.1: Extract instance IDs log.info("3.1: Extracting instance IDs") - machine_ids = [machine["machine_id"] for machine in status_response["requests"][0]["machines"]] + # HostFactory scheduler serialises instance IDs as camelCase "machineId"; + # the default scheduler uses snake_case "machine_id". Support both. + machine_ids = [ + machine.get("machineId") or machine.get("machine_id") + for machine in status_response["requests"][0]["machines"] + ] log.info(f"Machine IDs to return: {machine_ids}") # Determine resource ID @@ -1953,7 +2124,8 @@ def test_rest_api_control_loop(rest_api_client, setup_rest_api_environment, test return_response = rest_api_client.return_machines(machine_ids) log.debug(f"Return response: {json.dumps(return_response, indent=2)}") - return_request_id = return_response.get("request_id") + # HostFactory scheduler returns "requestId" (camelCase); default uses "request_id". + return_request_id = return_response.get("request_id") or return_response.get("requestId") if not return_request_id: log.warning(f"Return request ID missing in response: {return_response}") else: @@ -2029,13 +2201,13 @@ def test_rest_api_control_loop(rest_api_client, setup_rest_api_environment, test ) if not cleanup_verified: - log.error("⚠️ Cleanup verification failed - some resources may still exist") + log.error("Cleanup verification failed - some resources may still exist") for instance_id in machine_ids: state_info = get_instance_state(instance_id) if state_info["exists"]: log.error(f"Instance {instance_id} still exists in state: {state_info['state']}") else: - log.info("✅ All resources successfully cleaned up") + log.info("All resources successfully cleaned up") log.info("=" * 80) log.info(f"REST API test completed: {test_case['test_name']}") @@ -2062,8 +2234,10 @@ def test_rest_api_unknown_template_returns_error(setup_rest_api_environment): client.request_machines("NonExistent-Template-XYZ", 1) pytest.fail("Expected HTTPError for unknown template but no exception was raised") except _requests.HTTPError as exc: - assert exc.response.status_code >= 400, ( - f"Expected 4xx for unknown template, got {exc.response.status_code}" + response = exc.response + assert response is not None, "HTTPError raised with no response attached" + assert response.status_code >= 400, ( + f"Expected 4xx for unknown template, got {response.status_code}" ) except Exception as exc: # Any other exception is also acceptable — server rejected the request diff --git a/tests/onaws/test_sdk_onaws.py b/tests/providers/aws/live/test_sdk_onaws.py similarity index 79% rename from tests/onaws/test_sdk_onaws.py rename to tests/providers/aws/live/test_sdk_onaws.py index 22b589ed2..5bf051e6a 100644 --- a/tests/onaws/test_sdk_onaws.py +++ b/tests/providers/aws/live/test_sdk_onaws.py @@ -12,17 +12,17 @@ import boto3.session import pytest -from tests.onaws import scenarios -from tests.onaws.cleanup_helpers import ( +from tests.providers.aws.live import scenarios +from tests.providers.aws.live.cleanup_helpers import ( cleanup_launch_templates_for_request, get_machine_ids_from_ec2 as _get_machine_ids_from_ec2_helper, wait_for_instances_terminated, ) -from tests.onaws.scenarios import CUSTOM_TEST_CASES -from tests.onaws.template_processor import TemplateProcessor +from tests.providers.aws.live.scenarios import CUSTOM_TEST_CASES +from tests.providers.aws.live.template_processor import TemplateProcessor try: - from tests.onaws.scenarios_sdk import ( # type: ignore[import] + from tests.providers.aws.live.scenarios_sdk import ( # type: ignore[import] SDK_RUN_CUSTOM_CASES, SDK_RUN_DEFAULT_COMBINATIONS, SDK_TIMEOUTS, @@ -34,7 +34,7 @@ # Import AWS validation helpers from test_onaws (guarded for env/creds issues) try: - from tests.onaws.test_onaws import ( + from tests.providers.aws.live.test_onaws import ( _check_all_ec2_hosts_are_being_terminated, get_instance_state, ) @@ -56,14 +56,46 @@ os.environ["LOG_DESTINATION"] = "file" os.environ.setdefault("AWS_REGION", "eu-west-1") -_boto_session = boto3.session.Session() -_ec2_region = ( - os.environ.get("AWS_REGION") - or os.environ.get("AWS_DEFAULT_REGION") - or _boto_session.region_name - or "eu-west-1" -) -ec2_client = _boto_session.client("ec2", region_name=_ec2_region) +_ec2_client = None + + +def _get_boto_profile_and_region() -> tuple[str | None, str]: + """Read AWS profile and region from ORB_CONFIG_DIR config.""" + import json as _json + + profile = None + region = None + config_dir = os.environ.get("ORB_CONFIG_DIR") + if config_dir: + try: + config_path = os.path.join(config_dir, "config.json") + with open(config_path) as _f: + config = _json.load(_f) + providers = config.get("provider", {}).get("providers", []) + if providers: + provider_cfg = providers[0].get("config", {}) + profile = provider_cfg.get("profile") + region = provider_cfg.get("region") + except Exception: + pass + region = ( + region + or os.environ.get("AWS_REGION") + or os.environ.get("AWS_DEFAULT_REGION") + or "eu-west-1" + ) + return profile, region + + +def _get_ec2_client(): + global _ec2_client + if _ec2_client is None: + _profile, _region = _get_boto_profile_and_region() + _ec2_client = boto3.session.Session(profile_name=_profile, region_name=_region).client( + "ec2", region_name=_region + ) + return _ec2_client + log = logging.getLogger("sdk_test") log.setLevel(logging.DEBUG) @@ -115,15 +147,16 @@ def setup_sdk_test(request, test_session_id): "AWS_PROVIDER_LOG_DIR", "HF_LOGDIR", ] - os.environ["ORB_CONFIG_DIR"] = str(test_config_dir) - os.environ["HF_PROVIDER_CONFDIR"] = str(test_config_dir) + config_dir = test_config_dir / "config" + os.environ["ORB_CONFIG_DIR"] = str(config_dir) + os.environ["HF_PROVIDER_CONFDIR"] = str(config_dir) os.environ["HF_PROVIDER_LOGDIR"] = str(test_config_dir / "logs") os.environ["HF_PROVIDER_WORKDIR"] = str(test_config_dir / "work") os.environ["DEFAULT_PROVIDER_WORKDIR"] = str(test_config_dir / "work") os.environ["AWS_PROVIDER_LOG_DIR"] = str(test_config_dir / "logs") os.environ["HF_LOGDIR"] = str(test_config_dir / "logs") - config_path = str(test_config_dir / "config.json") + config_path = str(config_dir / "config.json") _tracked_request_ids: list[str] = [] @@ -181,14 +214,14 @@ async def _cleanup() -> None: for req_id in _tracked_request_ids for mid in _get_machine_ids_from_ec2(req_id) ], - ec2_client, + _get_ec2_client(), ) except Exception as exc: log.warning("Fixture teardown: wait_for_instances_terminated failed: %s", exc) for req_id in _tracked_request_ids: try: - cleanup_launch_templates_for_request(req_id, ec2_client) + cleanup_launch_templates_for_request(req_id, _get_ec2_client()) except Exception as exc: log.warning( "Fixture teardown: cleanup_launch_templates failed for %s: %s", req_id, exc @@ -214,7 +247,7 @@ async def _cleanup() -> None: def _get_machine_ids_from_ec2(request_id: str) -> list[str]: - return _get_machine_ids_from_ec2_helper(request_id, ec2_client) + return _get_machine_ids_from_ec2_helper(request_id, _get_ec2_client()) from tests.shared.response_helpers import ( @@ -309,17 +342,41 @@ async def _run_full_cycle(sdk, test_case: dict, tracked_request_ids: list[str]) ) machine_ids = _extract_machine_ids(status_response) - assert len(machine_ids) == capacity, ( - f"Expected {capacity} machines, got {len(machine_ids)}: {machine_ids}" + + # Capacity-unit fulfilment check from provider response fields. + # target_units / fulfilled_units reflect capacity units for weighted fleets + # and instance count for unweighted templates (1 unit == 1 instance). + _req0 = status_response.get("requests", [{}])[0] if isinstance(status_response, dict) else {} + target_units = _req0["target_units"] if _req0.get("target_units") is not None else capacity + fulfilled_units = ( + _req0["fulfilled_units"] if _req0.get("fulfilled_units") is not None else len(machine_ids) + ) + assert fulfilled_units >= target_units, ( + f"Fleet not fully fulfilled: fulfilled={fulfilled_units}, target={target_units}" ) + # Template-aware instance count sanity check. + if scenarios.template_uses_weighted_capacity(test_case): + assert len(machine_ids) >= 1, ( + f"Expected at least 1 machine (weighted template), got: {machine_ids}" + ) + else: + assert len(machine_ids) == capacity, ( + f"Expected {capacity} machines (unweighted template), got {len(machine_ids)}: {machine_ids}" + ) + for machine_id in machine_ids: state = get_instance_state(machine_id) assert state["exists"], f"Instance {machine_id} not found in AWS" assert state["state"] in ("running", "pending"), ( f"Instance {machine_id} in unexpected state: {state['state']}" ) - log.info("All %d instances provisioned: %s", capacity, machine_ids) + log.info( + "Provisioned %d instance(s) for capacity %d: %s", + len(machine_ids), + capacity, + machine_ids, + ) # 4. Return machines return_result = await sdk.create_return_request(machine_ids=machine_ids) # type: ignore[attr-defined] @@ -331,26 +388,19 @@ async def _run_full_cycle(sdk, test_case: dict, tracked_request_ids: list[str]) else getattr(return_result, "request_id", None) ) - # 5. Poll return completion + # 5. Poll return completion via get_request_status(return_request_id). + # The HostFactory getReturnRequests wire format flattens per-machine items + # without a per-request status, so the right way to ask "is this return + # complete?" under any scheduler is get_request_status(return_id). if return_request_id: deadline = time.time() + SDK_TIMEOUTS["return_completion"] + terminal = {"complete", "complete_with_error", "failed", "cancelled", "timeout"} while True: - ret_status = await sdk.list_return_requests() # type: ignore[attr-defined] - requests = ( - ret_status.get("requests", []) if isinstance(ret_status, dict) else ret_status or [] - ) - done = False - for req in requests: - rid = ( - req.get("requestId") or req.get("request_id") - if isinstance(req, dict) - else getattr(req, "request_id", None) - ) - s = req.get("status") if isinstance(req, dict) else getattr(req, "status", None) - if rid == return_request_id and s == "complete": - done = True - break - if done: + status_response = await sdk.get_request_status(request_id=return_request_id) # type: ignore[attr-defined] + status = _extract_request_status(status_response) + if status in terminal: + if status != "complete": + pytest.fail(f"Return request ended with non-success status: {status}") break if time.time() > deadline: pytest.fail("Timed out waiting for return request to complete") @@ -359,7 +409,7 @@ async def _run_full_cycle(sdk, test_case: dict, tracked_request_ids: list[str]) # 6. Assert AWS-side termination all_terminated = _check_all_ec2_hosts_are_being_terminated(machine_ids) assert all_terminated, f"Some instances not terminated: {machine_ids}" - log.info("All %d instances terminated", capacity) + log.info("All %d instance(s) terminated", len(machine_ids)) # --------------------------------------------------------------------------- diff --git a/tests/unit/providers/aws/cli/__init__.py b/tests/providers/aws/moto/__init__.py similarity index 100% rename from tests/unit/providers/aws/cli/__init__.py rename to tests/providers/aws/moto/__init__.py diff --git a/tests/providers/aws/moto/conftest.py b/tests/providers/aws/moto/conftest.py new file mode 100644 index 000000000..048b1a02a --- /dev/null +++ b/tests/providers/aws/moto/conftest.py @@ -0,0 +1,58 @@ +"""Moto-specific conftest for full-pipeline mocked AWS tests. + +Applies the moto compat patch to every test in this subtree and re-exports +helpers from the parent AWS conftest so that imports such as: + + from tests.providers.aws.moto.conftest import _inject_moto_factory + +continue to work without modification. +""" + +import pytest + +# Re-export helpers so existing imports remain valid +from tests.providers.aws.conftest import ( # noqa: F401 + REGION, + _inject_moto_factory, + _make_config_port, + _make_launch_template_manager, + _make_logger, + _make_moto_aws_client, + make_asg_handler, + make_aws_template, + make_ec2_fleet_handler, + make_patch_moto_compat, + make_request, + make_run_instances_handler, + make_spot_fleet_handler, +) + +__all__ = [ + "REGION", + "_inject_moto_factory", + "_make_config_port", + "_make_launch_template_manager", + "_make_logger", + "_make_moto_aws_client", + "make_asg_handler", + "make_aws_template", + "make_ec2_fleet_handler", + "make_patch_moto_compat", + "make_request", + "make_run_instances_handler", + "make_spot_fleet_handler", +] + + +@pytest.fixture(autouse=True) +def patch_moto_compat(): + """Patch moto-incompatible behaviours for all moto/ tests. + + 1. AWSImageResolutionService.is_resolution_needed -> False + Prevents SSM path resolution which moto cannot fulfil. + + 2. AWSProvisioningAdapter._provision_via_handlers synthesises instances + from instance_ids so the orchestration loop sees fulfilled_count > 0. + """ + with make_patch_moto_compat(): + yield diff --git a/tests/onmoto/test_asg_price_types.py b/tests/providers/aws/moto/test_asg_price_types.py similarity index 99% rename from tests/onmoto/test_asg_price_types.py rename to tests/providers/aws/moto/test_asg_price_types.py index 3a7b0cfb9..337a2f730 100644 --- a/tests/onmoto/test_asg_price_types.py +++ b/tests/providers/aws/moto/test_asg_price_types.py @@ -375,9 +375,9 @@ def test_check_status_asg_in_deleting_state_returns_empty(self, handler, vpc, as asg_client.delete_auto_scaling_group(AutoScalingGroupName=asg_name, ForceDelete=True) status_request = _make_request(request_id="asg-status-001", resource_ids=[asg_name]) - status = handler.check_hosts_status(status_request) + result = handler.check_hosts_status(status_request) - assert status == [] + assert result.instances == [] class TestASGTags: diff --git a/tests/onmoto/test_cleanup_e2e.py b/tests/providers/aws/moto/test_cleanup_e2e.py similarity index 100% rename from tests/onmoto/test_cleanup_e2e.py rename to tests/providers/aws/moto/test_cleanup_e2e.py diff --git a/tests/onmoto/test_cli_onmoto.py b/tests/providers/aws/moto/test_cli_onmoto.py similarity index 99% rename from tests/onmoto/test_cli_onmoto.py rename to tests/providers/aws/moto/test_cli_onmoto.py index 48894b1b8..2d0119e4e 100644 --- a/tests/onmoto/test_cli_onmoto.py +++ b/tests/providers/aws/moto/test_cli_onmoto.py @@ -25,7 +25,11 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) -from tests.onmoto.conftest import _inject_moto_factory, _make_logger, _make_moto_aws_client +from tests.providers.aws.moto.conftest import ( + _inject_moto_factory, + _make_logger, + _make_moto_aws_client, +) from tests.shared.constants import REQUEST_ID_RE from tests.shared.scenarios import TestScenario, get_smoke_scenarios diff --git a/tests/onmoto/test_client_template_format.py b/tests/providers/aws/moto/test_client_template_format.py similarity index 100% rename from tests/onmoto/test_client_template_format.py rename to tests/providers/aws/moto/test_client_template_format.py diff --git a/tests/onmoto/test_config_driven_provision.py b/tests/providers/aws/moto/test_config_driven_provision.py similarity index 100% rename from tests/onmoto/test_config_driven_provision.py rename to tests/providers/aws/moto/test_config_driven_provision.py diff --git a/tests/onmoto/test_cqrs_control_loop.py b/tests/providers/aws/moto/test_cqrs_control_loop.py similarity index 100% rename from tests/onmoto/test_cqrs_control_loop.py rename to tests/providers/aws/moto/test_cqrs_control_loop.py diff --git a/tests/onmoto/test_cross_cutting.py b/tests/providers/aws/moto/test_cross_cutting.py similarity index 100% rename from tests/onmoto/test_cross_cutting.py rename to tests/providers/aws/moto/test_cross_cutting.py diff --git a/tests/onmoto/test_di_wiring.py b/tests/providers/aws/moto/test_di_wiring.py similarity index 98% rename from tests/onmoto/test_di_wiring.py rename to tests/providers/aws/moto/test_di_wiring.py index e4e6634e2..6839ed6d1 100644 --- a/tests/onmoto/test_di_wiring.py +++ b/tests/providers/aws/moto/test_di_wiring.py @@ -200,7 +200,7 @@ def test_aws_template_accepts_allocation_strategy_as_string(): assert template.allocation_strategy_on_demand is not None # Must be the enum, not a raw string - from orb.providers.aws.domain.template.value_objects import AWSAllocationStrategy + from orb.providers.aws.value_objects import AWSAllocationStrategy assert isinstance(template.allocation_strategy_on_demand, AWSAllocationStrategy) @@ -213,7 +213,7 @@ def test_aws_template_accepts_allocation_strategy_as_string(): def test_aws_template_accepts_allocation_strategy_as_enum(): """AWSTemplate accepts an AWSAllocationStrategy object directly (no coercion needed).""" from orb.providers.aws.domain.template.aws_template_aggregate import AWSTemplate - from orb.providers.aws.domain.template.value_objects import AWSAllocationStrategy + from orb.providers.aws.value_objects import AWSAllocationStrategy strategy = AWSAllocationStrategy.from_string("lowestPrice") template = AWSTemplate( diff --git a/tests/onmoto/test_ec2fleet_price_types.py b/tests/providers/aws/moto/test_ec2fleet_price_types.py similarity index 99% rename from tests/onmoto/test_ec2fleet_price_types.py rename to tests/providers/aws/moto/test_ec2fleet_price_types.py index 9be241101..ccb3e7bc1 100644 --- a/tests/onmoto/test_ec2fleet_price_types.py +++ b/tests/providers/aws/moto/test_ec2fleet_price_types.py @@ -23,7 +23,7 @@ from orb.providers.aws.domain.template.aws_template_aggregate import AWSTemplate from orb.providers.aws.infrastructure.handlers.ec2_fleet.handler import EC2FleetHandler from orb.providers.aws.utilities.aws_operations import AWSOperations -from tests.onmoto.conftest import ( +from tests.providers.aws.moto.conftest import ( _make_config_port, _make_launch_template_manager, _make_logger, @@ -378,7 +378,7 @@ def test_check_status_deleted_fleet_returns_empty( ) result = handler.check_hosts_status(status_request) - assert result == [] + assert result.instances == [] # --------------------------------------------------------------------------- diff --git a/tests/onmoto/test_error_paths.py b/tests/providers/aws/moto/test_error_paths.py similarity index 95% rename from tests/onmoto/test_error_paths.py rename to tests/providers/aws/moto/test_error_paths.py index 7b5f18bc9..74fe4a993 100644 --- a/tests/onmoto/test_error_paths.py +++ b/tests/providers/aws/moto/test_error_paths.py @@ -13,7 +13,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) from orb.providers.aws.exceptions.aws_exceptions import AWSValidationError -from tests.onmoto.conftest import ( +from tests.providers.aws.moto.conftest import ( _make_config_port, _make_logger, _make_moto_aws_client as _make_aws_client, @@ -204,9 +204,11 @@ def test_release_nonexistent_instances_does_not_crash(self, asg_handler): def test_check_hosts_status_nonexistent_asg_returns_empty(self, asg_handler): """check_hosts_status for a non-existent ASG returns [] without crashing.""" + from orb.domain.base.provider_fulfilment import CheckHostsStatusResult + request = make_request(resource_ids=["asg-totally-fake-does-not-exist"]) result = asg_handler.check_hosts_status(request) - assert isinstance(result, list) + assert isinstance(result, CheckHostsStatusResult) def test_run_instances_invalid_ami_propagates_or_returns_failure( self, run_instances_handler, moto_vpc_resources @@ -310,7 +312,7 @@ def handler(self, moto_aws): aws_client = _make_aws_client(region=REGION) logger = _make_logger() config_port = _make_config_port(prefix="") - from tests.onmoto.conftest import make_ec2_fleet_handler + from tests.providers.aws.moto.conftest import make_ec2_fleet_handler return make_ec2_fleet_handler(aws_client, logger, config_port) @@ -348,7 +350,7 @@ def test_check_hosts_status_no_resource_ids_raises(self, handler): handler.check_hosts_status(request) def test_check_hosts_status_after_acquire_maintain(self, handler, vpc): - from tests.onmoto.conftest import make_ec2_fleet_handler + from tests.providers.aws.moto.conftest import make_ec2_fleet_handler aws_client = _make_aws_client(region=REGION) h = make_ec2_fleet_handler(aws_client, _make_logger(), _make_config_port()) @@ -359,9 +361,11 @@ def test_check_hosts_status_after_acquire_maintain(self, handler, vpc): acquire_result = h.acquire_hosts(request, template) fleet_id = acquire_result["resource_ids"][0] # type: ignore[index] + from orb.domain.base.provider_fulfilment import CheckHostsStatusResult + status_request = make_request(resource_ids=[fleet_id], metadata={"fleet_type": "maintain"}) result = h.check_hosts_status(status_request) - assert isinstance(result, list) + assert isinstance(result, CheckHostsStatusResult) def test_release_hosts_with_resource_mapping(self, handler, vpc): template = make_aws_template( @@ -388,7 +392,7 @@ def test_release_hosts_with_resource_mapping(self, handler, vpc): def _make_spot_handler_patched(moto_aws): - from tests.onmoto.conftest import make_spot_fleet_handler + from tests.providers.aws.moto.conftest import make_spot_fleet_handler aws_client = _make_aws_client(region=REGION) h = make_spot_fleet_handler(aws_client, _make_logger(), _make_config_port()) @@ -437,10 +441,12 @@ def test_acquire_fleet_missing_fleet_type_returns_failure(self, moto_aws, vpc): h.acquire_hosts(request, bad_template) def test_check_hosts_status_unknown_fleet_returns_empty(self, moto_aws): + from orb.domain.base.provider_fulfilment import CheckHostsStatusResult + h = _make_spot_handler_patched(moto_aws) request = make_request(resource_ids=["sfr-00000000-0000-0000-0000-000000000000"]) result = h.check_hosts_status(request) - assert isinstance(result, list) + assert isinstance(result, CheckHostsStatusResult) def test_release_hosts_with_resource_mapping(self, moto_aws, vpc): h = _make_spot_handler_patched(moto_aws) @@ -476,7 +482,7 @@ class TestRunInstancesHandlerEdgeCases: @pytest.fixture def handler(self, moto_aws): aws_client = _make_aws_client(region=REGION) - from tests.onmoto.conftest import make_run_instances_handler + from tests.providers.aws.moto.conftest import make_run_instances_handler return make_run_instances_handler(aws_client, _make_logger(), _make_config_port()) @@ -526,13 +532,13 @@ def test_check_hosts_status_falls_back_to_resource_ids(self, handler, vpc): ) result = handler.check_hosts_status(status_request) - assert isinstance(result, list) - assert len(result) == 1 + assert isinstance(result.instances, list) + assert len(result.instances) == 1 def test_check_hosts_status_no_ids_returns_empty(self, handler): request = make_request(resource_ids=[], provider_data={}) result = handler.check_hosts_status(request) - assert result == [] + assert result.instances == [] def test_check_hosts_status_multiple_instances(self, handler, vpc): template = make_aws_template(subnet_id=vpc["subnet_id"], sg_id=vpc["sg_id"]) @@ -549,8 +555,8 @@ def test_check_hosts_status_multiple_instances(self, handler, vpc): ) result = handler.check_hosts_status(status_request) - assert len(result) == len(instance_ids) - assert {r["instance_id"] for r in result} == set(instance_ids) + assert len(result.instances) == len(instance_ids) + assert {r["instance_id"] for r in result.instances} == set(instance_ids) def test_release_hosts_idempotent_on_already_terminated(self, handler, vpc): template = make_aws_template(subnet_id=vpc["subnet_id"], sg_id=vpc["sg_id"]) diff --git a/tests/onmoto/test_hf_contract.py b/tests/providers/aws/moto/test_hf_contract.py similarity index 98% rename from tests/onmoto/test_hf_contract.py rename to tests/providers/aws/moto/test_hf_contract.py index 41209eb0e..b90e7ff42 100644 --- a/tests/onmoto/test_hf_contract.py +++ b/tests/providers/aws/moto/test_hf_contract.py @@ -18,7 +18,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) -from tests.onaws.plugin_io_schemas import ( +from tests.providers.aws.live.plugin_io_schemas import ( expected_get_available_templates_schema_default, expected_get_available_templates_schema_hostfactory, expected_request_machines_schema_default, @@ -202,7 +202,8 @@ def _build_request_dto_from_run_instances( resource_ids=[reservation_id], provider_data={"instance_ids": instance_ids, "reservation_id": reservation_id}, ) - machine_data_list = handler.check_hosts_status(status_request) + check_result = handler.check_hosts_status(status_request) + machine_data_list = check_result.instances # Build MachineReferenceDTOs from raw machine dicts returned by check_hosts_status machine_refs = [] @@ -488,7 +489,8 @@ def test_default_status_schema_with_run_instances_machines( resource_ids=[reservation_id], provider_data={"instance_ids": instance_ids, "reservation_id": reservation_id}, ) - machine_data_list = handler.check_hosts_status(status_request) + check_result = handler.check_hosts_status(status_request) + machine_data_list = check_result.instances machines = [] for m in machine_data_list: diff --git a/tests/onmoto/test_init_discovery_onmoto.py b/tests/providers/aws/moto/test_init_discovery_onmoto.py similarity index 100% rename from tests/onmoto/test_init_discovery_onmoto.py rename to tests/providers/aws/moto/test_init_discovery_onmoto.py diff --git a/tests/onmoto/test_launch_template_existing.py b/tests/providers/aws/moto/test_launch_template_existing.py similarity index 100% rename from tests/onmoto/test_launch_template_existing.py rename to tests/providers/aws/moto/test_launch_template_existing.py diff --git a/tests/onmoto/test_mcp_onmoto.py b/tests/providers/aws/moto/test_mcp_onmoto.py similarity index 99% rename from tests/onmoto/test_mcp_onmoto.py rename to tests/providers/aws/moto/test_mcp_onmoto.py index 81149aad5..cd75b703e 100644 --- a/tests/onmoto/test_mcp_onmoto.py +++ b/tests/providers/aws/moto/test_mcp_onmoto.py @@ -24,7 +24,11 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) -from tests.onmoto.conftest import _inject_moto_factory, _make_logger, _make_moto_aws_client +from tests.providers.aws.moto.conftest import ( + _inject_moto_factory, + _make_logger, + _make_moto_aws_client, +) from tests.shared.constants import REQUEST_ID_RE from tests.shared.scenarios import TestScenario, get_smoke_scenarios diff --git a/tests/onmoto/test_multi_resource_onmoto.py b/tests/providers/aws/moto/test_multi_resource_onmoto.py similarity index 100% rename from tests/onmoto/test_multi_resource_onmoto.py rename to tests/providers/aws/moto/test_multi_resource_onmoto.py diff --git a/tests/onmoto/test_partial_return.py b/tests/providers/aws/moto/test_partial_return.py similarity index 99% rename from tests/onmoto/test_partial_return.py rename to tests/providers/aws/moto/test_partial_return.py index 1a67f5747..4f990a02a 100644 --- a/tests/onmoto/test_partial_return.py +++ b/tests/providers/aws/moto/test_partial_return.py @@ -272,9 +272,9 @@ def test_check_status_after_partial_return_reflects_mixed_states(self, acquired_ ) result = handler.check_hosts_status(status_request) - assert len(result) == 3 + assert len(result.instances) == 3 - statuses = {entry["instance_id"]: entry["status"] for entry in result} + statuses = {entry["instance_id"]: entry["status"] for entry in result.instances} terminated_entry = statuses[instance_ids[0]] assert terminated_entry in ("terminated", "shutting-down", "stopping") diff --git a/tests/onmoto/test_provision_lifecycle.py b/tests/providers/aws/moto/test_provision_lifecycle.py similarity index 97% rename from tests/onmoto/test_provision_lifecycle.py rename to tests/providers/aws/moto/test_provision_lifecycle.py index 8f6af26b4..b268f2566 100644 --- a/tests/onmoto/test_provision_lifecycle.py +++ b/tests/providers/aws/moto/test_provision_lifecycle.py @@ -316,11 +316,13 @@ def test_check_status_after_acquire(self, factory, subnet_id, sg_id): acquire_result = handler.acquire_hosts(request, template) asg_name = acquire_result["resource_ids"][0] + from orb.domain.base.provider_fulfilment import CheckHostsStatusResult + status_request = _make_request(resource_ids=[asg_name]) result = handler.check_hosts_status(status_request) # moto does not spin up ASG instances automatically - assert isinstance(result, list) + assert isinstance(result, CheckHostsStatusResult) def test_release_after_acquire(self, factory, subnet_id, sg_id, autoscaling_client): """release_hosts with empty machine_ids does not raise after acquire.""" @@ -375,10 +377,12 @@ def test_check_status_after_acquire(self, factory, subnet_id, sg_id): acquire_result = handler.acquire_hosts(request, template) fleet_id = acquire_result["resource_ids"][0] + from orb.domain.base.provider_fulfilment import CheckHostsStatusResult + status_request = _make_request(resource_ids=[fleet_id]) result = handler.check_hosts_status(status_request) - assert isinstance(result, list) + assert isinstance(result, CheckHostsStatusResult) def test_release_after_acquire(self, factory, subnet_id, sg_id): """release_hosts with empty machine_ids does not raise.""" @@ -432,10 +436,12 @@ def test_check_status_after_acquire(self, factory, subnet_id, sg_id): acquire_result = handler.acquire_hosts(request, template) fleet_id = acquire_result["resource_ids"][0] + from orb.domain.base.provider_fulfilment import CheckHostsStatusResult + status_request = _make_request(resource_ids=[fleet_id]) result = handler.check_hosts_status(status_request) - assert isinstance(result, list) + assert isinstance(result, CheckHostsStatusResult) def test_release_after_acquire(self, factory, subnet_id, sg_id): """release_hosts with empty machine_ids does not raise.""" @@ -500,8 +506,8 @@ def test_check_status_returns_instance_data(self, factory, subnet_id, sg_id): ) result = handler.check_hosts_status(status_request) - assert len(result) == len(instance_ids) - assert result[0]["instance_id"] == instance_ids[0] + assert len(result.instances) == len(instance_ids) + assert result.instances[0]["instance_id"] == instance_ids[0] def test_release_terminates_instances(self, factory, subnet_id, sg_id, ec2_client): """release_hosts terminates the launched instances.""" @@ -542,7 +548,7 @@ def test_full_lifecycle(self, factory, subnet_id, sg_id, ec2_client): provider_data={"instance_ids": instance_ids, "reservation_id": reservation_id}, ) status = handler.check_hosts_status(status_request) - assert len(status) >= 1 + assert len(status.instances) >= 1 # Release handler.release_hosts(instance_ids) diff --git a/tests/onmoto/test_rest_api_onmoto.py b/tests/providers/aws/moto/test_rest_api_onmoto.py similarity index 99% rename from tests/onmoto/test_rest_api_onmoto.py rename to tests/providers/aws/moto/test_rest_api_onmoto.py index b977cf184..fdc6d839c 100644 --- a/tests/onmoto/test_rest_api_onmoto.py +++ b/tests/providers/aws/moto/test_rest_api_onmoto.py @@ -19,7 +19,11 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) -from tests.onmoto.conftest import _inject_moto_factory, _make_logger, _make_moto_aws_client +from tests.providers.aws.moto.conftest import ( + _inject_moto_factory, + _make_logger, + _make_moto_aws_client, +) from tests.shared.constants import REQUEST_ID_RE from tests.shared.scenarios import TestScenario, get_smoke_scenarios diff --git a/tests/onmoto/test_return_flow.py b/tests/providers/aws/moto/test_return_flow.py similarity index 100% rename from tests/onmoto/test_return_flow.py rename to tests/providers/aws/moto/test_return_flow.py diff --git a/tests/onmoto/test_run_instances_gaps.py b/tests/providers/aws/moto/test_run_instances_gaps.py similarity index 97% rename from tests/onmoto/test_run_instances_gaps.py rename to tests/providers/aws/moto/test_run_instances_gaps.py index 661324834..76b99abfc 100644 --- a/tests/onmoto/test_run_instances_gaps.py +++ b/tests/providers/aws/moto/test_run_instances_gaps.py @@ -325,10 +325,10 @@ def test_check_status_instance_in_stopped_state( resource_ids=[reservation_id], provider_data={"instance_ids": instance_ids, "reservation_id": reservation_id}, ) - status = handler.check_hosts_status(status_request) + result = handler.check_hosts_status(status_request) - assert len(status) == len(instance_ids) - assert status[0]["instance_id"] == instance_ids[0] + assert len(result.instances) == len(instance_ids) + assert result.instances[0]["instance_id"] == instance_ids[0] def test_check_status_instance_in_terminated_state( self, handler: RunInstancesHandler, subnet_id: str, sg_id: str, ec2: Any @@ -348,10 +348,10 @@ def test_check_status_instance_in_terminated_state( resource_ids=[reservation_id], provider_data={"instance_ids": instance_ids, "reservation_id": reservation_id}, ) - status = handler.check_hosts_status(status_request) + result = handler.check_hosts_status(status_request) - assert len(status) == len(instance_ids) - terminated_statuses = {entry["status"] for entry in status} + assert len(result.instances) == len(instance_ids) + terminated_statuses = {entry["status"] for entry in result.instances} assert terminated_statuses & {"terminated", "shutting-down", "stopping"} def test_release_partial_then_full_two_step( diff --git a/tests/onmoto/test_sdk_default_scheduler.py b/tests/providers/aws/moto/test_sdk_default_scheduler.py similarity index 99% rename from tests/onmoto/test_sdk_default_scheduler.py rename to tests/providers/aws/moto/test_sdk_default_scheduler.py index dec3ebb5c..e2fab6560 100644 --- a/tests/onmoto/test_sdk_default_scheduler.py +++ b/tests/providers/aws/moto/test_sdk_default_scheduler.py @@ -163,7 +163,7 @@ def orb_config_dir_default(tmp_path, moto_vpc_resources): # Generate snake_case templates via the default scheduler strategy try: - from tests.onaws.template_processor import TemplateProcessor + from tests.providers.aws.live.template_processor import TemplateProcessor templates_data = TemplateProcessor.generate_templates_programmatically("default") except Exception as exc: diff --git a/tests/onmoto/test_sdk_onmoto.py b/tests/providers/aws/moto/test_sdk_onmoto.py similarity index 99% rename from tests/onmoto/test_sdk_onmoto.py rename to tests/providers/aws/moto/test_sdk_onmoto.py index c940eec77..edc1a59be 100644 --- a/tests/onmoto/test_sdk_onmoto.py +++ b/tests/providers/aws/moto/test_sdk_onmoto.py @@ -20,7 +20,11 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) -from tests.onmoto.conftest import _inject_moto_factory, _make_logger, _make_moto_aws_client +from tests.providers.aws.moto.conftest import ( + _inject_moto_factory, + _make_logger, + _make_moto_aws_client, +) from tests.shared.constants import REQUEST_ID_RE from tests.shared.scenarios import TestScenario, get_smoke_scenarios diff --git a/tests/onmoto/test_spot_fleet_gaps.py b/tests/providers/aws/moto/test_spot_fleet_gaps.py similarity index 99% rename from tests/onmoto/test_spot_fleet_gaps.py rename to tests/providers/aws/moto/test_spot_fleet_gaps.py index c5ae8dfcd..91aa75f47 100644 --- a/tests/onmoto/test_spot_fleet_gaps.py +++ b/tests/providers/aws/moto/test_spot_fleet_gaps.py @@ -31,7 +31,7 @@ ) from orb.providers.aws.infrastructure.handlers.spot_fleet.handler import SpotFleetHandler from orb.providers.aws.utilities.aws_operations import AWSOperations -from tests.onmoto.conftest import ( +from tests.providers.aws.moto.conftest import ( REGION, _make_config_port, _make_launch_template_manager, @@ -387,9 +387,9 @@ def test_check_status_cancelled_fleet_returns_empty( ) status_request = _make_request(request_id="spot-status-cancel", resource_ids=[fleet_id]) - status = handler.check_hosts_status(status_request) + result = handler.check_hosts_status(status_request) - assert status == [] + assert result.instances == [] # --------------------------------------------------------------------------- diff --git a/tests/onmoto/test_template_pipeline.py b/tests/providers/aws/moto/test_template_pipeline.py similarity index 96% rename from tests/onmoto/test_template_pipeline.py rename to tests/providers/aws/moto/test_template_pipeline.py index a93cd1f05..43af10696 100644 --- a/tests/onmoto/test_template_pipeline.py +++ b/tests/providers/aws/moto/test_template_pipeline.py @@ -180,14 +180,20 @@ def test_default_strategy_loads_snake_case_passthrough(tmp_path): def test_default_strategy_delegates_hf_file_to_hf_strategy(tmp_path): """Default strategy detects scheduler_type=hostfactory and delegates via registry when registered.""" + from orb.infrastructure.scheduler.registration import ( + register_default_scheduler, + register_symphony_hostfactory_scheduler, + ) from orb.infrastructure.scheduler.registry import get_scheduler_registry registry = get_scheduler_registry() - # Register both types so delegation can resolve the HF strategy class + # Use the canonical registration helpers so strategy_class is populated correctly. + # register_type is idempotent, so these are safe to call even if a prior test already + # registered the types via the DI container boot path. if not registry.is_registered("hostfactory"): - registry.register("hostfactory", HostFactorySchedulerStrategy, lambda c: None) + register_symphony_hostfactory_scheduler(registry) if not registry.is_registered("default"): - registry.register("default", DefaultSchedulerStrategy, lambda c: None) + register_default_scheduler(registry) tpl_file = tmp_path / "aws_templates.json" _write_hf_file(tpl_file, [_MINIMAL_HF_TEMPLATE]) diff --git a/tests/unit/providers/aws/configuration/__init__.py b/tests/providers/aws/unit/__init__.py similarity index 100% rename from tests/unit/providers/aws/configuration/__init__.py rename to tests/providers/aws/unit/__init__.py diff --git a/tests/unit/providers/aws/handlers/__init__.py b/tests/providers/aws/unit/cli/__init__.py similarity index 100% rename from tests/unit/providers/aws/handlers/__init__.py rename to tests/providers/aws/unit/cli/__init__.py diff --git a/tests/unit/providers/aws/cli/test_aws_cli_spec.py b/tests/providers/aws/unit/cli/test_aws_cli_spec.py similarity index 100% rename from tests/unit/providers/aws/cli/test_aws_cli_spec.py rename to tests/providers/aws/unit/cli/test_aws_cli_spec.py diff --git a/tests/unit/providers/aws/infrastructure/__init__.py b/tests/providers/aws/unit/configuration/__init__.py similarity index 100% rename from tests/unit/providers/aws/infrastructure/__init__.py rename to tests/providers/aws/unit/configuration/__init__.py diff --git a/tests/unit/providers/aws/configuration/test_aws_batch_sizes_config.py b/tests/providers/aws/unit/configuration/test_aws_batch_sizes_config.py similarity index 100% rename from tests/unit/providers/aws/configuration/test_aws_batch_sizes_config.py rename to tests/providers/aws/unit/configuration/test_aws_batch_sizes_config.py diff --git a/tests/unit/providers/aws/configuration/test_aws_naming_config.py b/tests/providers/aws/unit/configuration/test_aws_naming_config.py similarity index 100% rename from tests/unit/providers/aws/configuration/test_aws_naming_config.py rename to tests/providers/aws/unit/configuration/test_aws_naming_config.py diff --git a/tests/unit/providers/aws/configuration/test_cleanup_config.py b/tests/providers/aws/unit/configuration/test_cleanup_config.py similarity index 100% rename from tests/unit/providers/aws/configuration/test_cleanup_config.py rename to tests/providers/aws/unit/configuration/test_cleanup_config.py diff --git a/tests/unit/providers/aws/infrastructure/handlers/__init__.py b/tests/providers/aws/unit/handlers/__init__.py similarity index 100% rename from tests/unit/providers/aws/infrastructure/handlers/__init__.py rename to tests/providers/aws/unit/handlers/__init__.py diff --git a/tests/unit/providers/aws/handlers/test_base_handler_circuit_breaker_config.py b/tests/providers/aws/unit/handlers/test_base_handler_circuit_breaker_config.py similarity index 100% rename from tests/unit/providers/aws/handlers/test_base_handler_circuit_breaker_config.py rename to tests/providers/aws/unit/handlers/test_base_handler_circuit_breaker_config.py diff --git a/tests/unit/providers/aws/handlers/test_fleet_release_regression.py b/tests/providers/aws/unit/handlers/test_fleet_release_regression.py similarity index 100% rename from tests/unit/providers/aws/handlers/test_fleet_release_regression.py rename to tests/providers/aws/unit/handlers/test_fleet_release_regression.py diff --git a/tests/unit/providers/aws/storage/__init__.py b/tests/providers/aws/unit/infrastructure/__init__.py similarity index 100% rename from tests/unit/providers/aws/storage/__init__.py rename to tests/providers/aws/unit/infrastructure/__init__.py diff --git a/tests/unit/providers/aws/strategy/__init__.py b/tests/providers/aws/unit/infrastructure/handlers/__init__.py similarity index 100% rename from tests/unit/providers/aws/strategy/__init__.py rename to tests/providers/aws/unit/infrastructure/handlers/__init__.py diff --git a/tests/unit/providers/aws/utilities/__init__.py b/tests/providers/aws/unit/infrastructure/handlers/shared/__init__.py similarity index 100% rename from tests/unit/providers/aws/utilities/__init__.py rename to tests/providers/aws/unit/infrastructure/handlers/shared/__init__.py diff --git a/tests/providers/aws/unit/infrastructure/handlers/shared/test_fleet_fulfilment_helper.py b/tests/providers/aws/unit/infrastructure/handlers/shared/test_fleet_fulfilment_helper.py new file mode 100644 index 000000000..92a4bc538 --- /dev/null +++ b/tests/providers/aws/unit/infrastructure/handlers/shared/test_fleet_fulfilment_helper.py @@ -0,0 +1,129 @@ +"""Unit tests for the compute_capacity_based_fulfilment shared helper.""" + +from __future__ import annotations + +import pytest + +from orb.domain.base.provider_fulfilment import ProviderFulfilment +from orb.providers.aws.infrastructure.handlers.shared.fleet_fulfilment import ( + compute_capacity_based_fulfilment, +) + + +@pytest.mark.unit +def test_fully_fulfilled_returns_fulfilled_state(): + """target_capacity met, all running, no pending/failed → fulfilled.""" + result = compute_capacity_based_fulfilment( + target_capacity=10, + fulfilled_capacity=10.0, + running_count=10, + pending_count=0, + failed_count=0, + provider_label="Fleet", + ) + assert isinstance(result, ProviderFulfilment) + assert result.state == "fulfilled" + assert result.running_count == 10 + assert result.pending_count == 0 + assert result.failed_count == 0 + assert result.fulfilled_units == 10 + assert result.target_units == 10 + + +@pytest.mark.unit +def test_in_progress_when_pending_present(): + """fulfilled_capacity < target or pending instances → in_progress.""" + result = compute_capacity_based_fulfilment( + target_capacity=10, + fulfilled_capacity=4.0, + running_count=4, + pending_count=6, + failed_count=0, + provider_label="Fleet", + ) + assert result.state == "in_progress" + assert result.running_count == 4 + assert result.pending_count == 6 + assert result.failed_count == 0 + + +@pytest.mark.unit +def test_partial_when_failed_present_no_pending(): + """failed > 0 AND running == 0 AND pending == 0 → failed state (not partial).""" + # The implementation: failed>0, running==0, pending==0 → "failed" branch. + # When there is a mix (some running, some failed, no pending) → in_progress/else branch. + # Test the pure-failed case first. + result = compute_capacity_based_fulfilment( + target_capacity=5, + fulfilled_capacity=0.0, + running_count=0, + pending_count=0, + failed_count=5, + provider_label="Fleet", + ) + assert result.state == "failed" + assert result.failed_count == 5 + assert result.running_count == 0 + + +@pytest.mark.unit +def test_in_progress_when_some_running_some_failed_no_pending(): + """Some running + some failed + no pending → in_progress (else branch).""" + result = compute_capacity_based_fulfilment( + target_capacity=10, + fulfilled_capacity=3.0, + running_count=3, + pending_count=0, + failed_count=2, + provider_label="Spot Fleet", + ) + # failed_count > 0 but running_count > 0 → does NOT hit the pure-failed branch + assert result.state == "in_progress" + + +@pytest.mark.unit +def test_provider_label_appears_in_message(): + """The provider_label value must appear verbatim in the returned message.""" + result = compute_capacity_based_fulfilment( + target_capacity=2, + fulfilled_capacity=2.0, + running_count=2, + pending_count=0, + failed_count=0, + provider_label="Spot Fleet", + ) + assert "Spot Fleet" in result.message + + +@pytest.mark.unit +def test_zero_target_capacity_handles_gracefully(): + """target_capacity=0 with fulfilled_capacity=0 must not raise.""" + # fulfilled_capacity (0.0) >= target_capacity (0) is True, and pending==0, + # failed==0 → fulfilled branch. + result = compute_capacity_based_fulfilment( + target_capacity=0, + fulfilled_capacity=0.0, + running_count=0, + pending_count=0, + failed_count=0, + provider_label="Fleet", + ) + assert result.state == "fulfilled" + assert result.target_units == 0 + assert result.fulfilled_units == 0 + + +@pytest.mark.unit +def test_none_target_capacity_uses_fulfilled_as_target(): + """When target_capacity is None, fulfilled_capacity is used as the target.""" + result = compute_capacity_based_fulfilment( + target_capacity=None, + fulfilled_capacity=4.0, + running_count=4, + pending_count=0, + failed_count=0, + provider_label="Fleet", + ) + # target_capacity is None → fleet_fully_fulfilled is False → in_progress/else branch. + assert result.state == "in_progress" + assert result.target_units == 4 # derived from int(fulfilled_capacity) diff --git a/tests/unit/providers/aws/infrastructure/handlers/test_asg_handler.py b/tests/providers/aws/unit/infrastructure/handlers/test_asg_handler.py similarity index 56% rename from tests/unit/providers/aws/infrastructure/handlers/test_asg_handler.py rename to tests/providers/aws/unit/infrastructure/handlers/test_asg_handler.py index b4e4af65a..025b23d18 100644 --- a/tests/unit/providers/aws/infrastructure/handlers/test_asg_handler.py +++ b/tests/providers/aws/unit/infrastructure/handlers/test_asg_handler.py @@ -5,6 +5,11 @@ from botocore.exceptions import ClientError +from orb.domain.base.provider_fulfilment import ( + CheckHostsStatusResult, + FulfilmentState, + ProviderFulfilment, +) from orb.providers.aws.exceptions.aws_exceptions import AWSInfrastructureError from orb.providers.aws.infrastructure.handlers.asg.handler import ASGHandler @@ -19,11 +24,12 @@ def _make_handler() -> Any: return handler -def _make_request(resource_ids): +def _make_request(resource_ids, requested_count=2): request = MagicMock() request.request_id = "req-asg-123" request.resource_ids = resource_ids request.metadata = {} + request.requested_count = requested_count return request @@ -32,7 +38,7 @@ def _make_client_error(code="InternalError"): def _formatted_instances(instance_ids, resource_id="asg-test"): - """Return already-formatted instance dicts (as _get_asg_instances returns them).""" + """Return already-formatted instance dicts.""" return [ { "instance_id": iid, @@ -51,157 +57,176 @@ def _formatted_instances(instance_ids, resource_id="asg-test"): ] +def _asg_result(instance_ids, resource_id="asg-test", state: FulfilmentState = "fulfilled"): + """Build a CheckHostsStatusResult for mocking _get_asg_status.""" + return CheckHostsStatusResult( + instances=_formatted_instances(instance_ids, resource_id), + fulfilment=ProviderFulfilment(state=state, message="test"), + ) + + class TestASGHandlerCheckHostsStatus: def test_check_hosts_status_all_inservice(self): - """All InService instances → returns all.""" + """All InService instances → CheckHostsStatusResult with all instances and fulfilled state.""" handler = _make_handler() request = _make_request(["asg-111"]) instance_ids = ["i-asg1", "i-asg2"] with patch.object( handler, - "_get_asg_instances", - return_value=_formatted_instances(instance_ids, "asg-111"), + "_get_asg_status", + return_value=_asg_result(instance_ids, "asg-111", state="fulfilled"), ): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + result = handler.check_hosts_status(request) - assert len(result) == 2 - returned_ids = {r["instance_id"] for r in result} + assert isinstance(result, CheckHostsStatusResult) + assert isinstance(result.fulfilment, ProviderFulfilment) + assert len(result.instances) == 2 + returned_ids = {r["instance_id"] for r in result.instances} assert returned_ids == set(instance_ids) + assert result.fulfilment.state == "fulfilled" def test_check_hosts_status_mixed_lifecycle(self): - """_get_asg_instances extracts all instance IDs from ASG regardless of lifecycle state.""" + """ASG with mixed lifecycle states — handler reports what _get_asg_status returns.""" handler = _make_handler() request = _make_request(["asg-222"]) - # ASG returns InService + Terminating instances — handler returns all from _get_asg_instances all_ids = ["i-inservice1", "i-inservice2", "i-terminating1"] with patch.object( - handler, "_get_asg_instances", return_value=_formatted_instances(all_ids, "asg-222") + handler, + "_get_asg_status", + return_value=_asg_result(all_ids, "asg-222", state="in_progress"), ): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + result = handler.check_hosts_status(request) - assert len(result) == 3 + assert isinstance(result, CheckHostsStatusResult) + assert isinstance(result.fulfilment, ProviderFulfilment) + assert len(result.instances) == 3 def test_check_hosts_status_asg_not_found(self): - """_get_asg_instances returns [] when ASG not found → result is [].""" + """_get_asg_status raises → exception logged and skipped; result has empty instances and in_progress.""" handler = _make_handler() request = _make_request(["asg-missing"]) - with patch.object(handler, "_get_asg_instances", return_value=[]): + with patch.object( + handler, "_get_asg_status", side_effect=AWSInfrastructureError("ASG not found") + ): result = handler.check_hosts_status(request) - assert result == [] + assert isinstance(result, CheckHostsStatusResult) + assert isinstance(result.fulfilment, ProviderFulfilment) + assert result.instances == [] + assert result.fulfilment.state == "in_progress" def test_check_hosts_status_aws_error(self): - """Exception inside per-ASG loop → logged and skipped; result is [].""" + """Exception inside per-ASG loop → logged and skipped; empty instances, in_progress.""" handler = _make_handler() request = _make_request(["asg-err"]) with patch.object( - handler, "_get_asg_instances", side_effect=AWSInfrastructureError("AWS error") + handler, "_get_asg_status", side_effect=AWSInfrastructureError("AWS error") ): result = handler.check_hosts_status(request) - assert result == [] + assert isinstance(result, CheckHostsStatusResult) + assert isinstance(result.fulfilment, ProviderFulfilment) + assert result.instances == [] + assert result.fulfilment.state == "in_progress" def test_check_hosts_status_no_resource_ids(self): - """Empty resource_ids → returns [] immediately without calling AWS.""" + """Empty resource_ids → CheckHostsStatusResult with empty instances and in_progress, no AWS calls.""" handler = _make_handler() request = _make_request([]) - with patch.object(handler, "_get_asg_instances") as mock_get: + with patch.object(handler, "_get_asg_status") as mock_get: result = handler.check_hosts_status(request) - assert result == [] + assert isinstance(result, CheckHostsStatusResult) + assert isinstance(result.fulfilment, ProviderFulfilment) + assert result.instances == [] + assert result.fulfilment.state == "in_progress" mock_get.assert_not_called() def test_check_hosts_status_returns_correct_count(self): - """Verify count matches instances in ASG.""" + """Verify instance count in result.instances matches instances returned by _get_asg_status.""" handler = _make_handler() request = _make_request(["asg-cnt"]) instance_ids = ["i-cnt1", "i-cnt2", "i-cnt3"] with patch.object( handler, - "_get_asg_instances", - return_value=_formatted_instances(instance_ids, "asg-cnt"), + "_get_asg_status", + return_value=_asg_result(instance_ids, "asg-cnt", state="fulfilled"), ): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + result = handler.check_hosts_status(request) - assert len(result) == 3 + assert isinstance(result, CheckHostsStatusResult) + assert isinstance(result.fulfilment, ProviderFulfilment) + assert len(result.instances) == 3 + assert result.fulfilment.state == "fulfilled" def test_check_hosts_status_preserves_instance_ids(self): - """Instance IDs in result match input.""" + """Instance IDs in result.instances match input.""" handler = _make_handler() request = _make_request(["asg-ids"]) instance_ids = ["i-asg-preserve1", "i-asg-preserve2"] with patch.object( handler, - "_get_asg_instances", - return_value=_formatted_instances(instance_ids, "asg-ids"), + "_get_asg_status", + return_value=_asg_result(instance_ids, "asg-ids", state="fulfilled"), ): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + result = handler.check_hosts_status(request) - returned_ids = {r["instance_id"] for r in result} + assert isinstance(result, CheckHostsStatusResult) + assert isinstance(result.fulfilment, ProviderFulfilment) + returned_ids = {r["instance_id"] for r in result.instances} assert returned_ids == set(instance_ids) + assert result.fulfilment.state == "fulfilled" def test_check_hosts_status_multiple_asgs(self): - """Multiple ASG names in resource_ids → aggregates results from all.""" + """Multiple ASG names in resource_ids → aggregates instances from all ASGs.""" handler = _make_handler() request = _make_request(["asg-A", "asg-B"]) ids_a = ["i-a1", "i-a2"] ids_b = ["i-b1"] - def get_asg_instances_side_effect(asg_name, **kwargs): + def get_asg_status_side_effect(asg_name, **kwargs): if asg_name == "asg-A": - return _formatted_instances(ids_a, "asg-A") - return _formatted_instances(ids_b, "asg-B") + return _asg_result(ids_a, "asg-A", state="fulfilled") + return _asg_result(ids_b, "asg-B", state="fulfilled") - with patch.object(handler, "_get_asg_instances", side_effect=get_asg_instances_side_effect): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + with patch.object(handler, "_get_asg_status", side_effect=get_asg_status_side_effect): + result = handler.check_hosts_status(request) - assert len(result) == 3 - returned_ids = {r["instance_id"] for r in result} + assert isinstance(result, CheckHostsStatusResult) + assert isinstance(result.fulfilment, ProviderFulfilment) + assert len(result.instances) == 3 + returned_ids = {r["instance_id"] for r in result.instances} assert returned_ids == {"i-a1", "i-a2", "i-b1"} + assert result.fulfilment.state == "fulfilled" def test_check_hosts_status_state_filtering_is_strict(self): - """Only instances returned by _get_asg_instances appear in result.""" + """Only instances returned by _get_asg_status appear in result.instances.""" handler = _make_handler() request = _make_request(["asg-strict"]) - # Only one instance is active active_ids = ["i-strict-active"] with patch.object( handler, - "_get_asg_instances", - return_value=_formatted_instances(active_ids, "asg-strict"), + "_get_asg_status", + return_value=_asg_result(active_ids, "asg-strict", state="fulfilled"), ): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + result = handler.check_hosts_status(request) - assert len(result) == 1 - assert result[0]["instance_id"] == "i-strict-active" + assert isinstance(result, CheckHostsStatusResult) + assert isinstance(result.fulfilment, ProviderFulfilment) + assert len(result.instances) == 1 + assert result.instances[0]["instance_id"] == "i-strict-active" + assert result.fulfilment.state == "fulfilled" + assert result.fulfilment.target_units is None class TestASGHandlerNameTag: diff --git a/tests/unit/providers/aws/infrastructure/handlers/test_base_handler_fallback.py b/tests/providers/aws/unit/infrastructure/handlers/test_base_handler_fallback.py similarity index 100% rename from tests/unit/providers/aws/infrastructure/handlers/test_base_handler_fallback.py rename to tests/providers/aws/unit/infrastructure/handlers/test_base_handler_fallback.py diff --git a/tests/unit/providers/aws/infrastructure/handlers/test_cleanup.py b/tests/providers/aws/unit/infrastructure/handlers/test_cleanup.py similarity index 96% rename from tests/unit/providers/aws/infrastructure/handlers/test_cleanup.py rename to tests/providers/aws/unit/infrastructure/handlers/test_cleanup.py index fb5222e7f..6a8a587ed 100644 --- a/tests/unit/providers/aws/infrastructure/handlers/test_cleanup.py +++ b/tests/providers/aws/unit/infrastructure/handlers/test_cleanup.py @@ -310,6 +310,11 @@ def test_partial_return_does_not_trigger_lt_cleanup(self): with ( patch.object(handler._fleet_release_manager, "_delete_fleet") as mock_delete_fleet, patch.object(handler, "_delete_orb_launch_template") as mock_delete_lt, + patch.object( + handler._fleet_release_manager, + "_fleet_has_no_remaining_instances", + return_value=False, + ), ): handler._release_hosts_for_single_ec2_fleet( "fleet-123", @@ -390,7 +395,14 @@ def test_partial_return_does_not_trigger_lt_cleanup(self): config_port = _make_config_port() handler = _make_spot_fleet_handler(config_port=config_port) - with patch.object(handler, "_delete_orb_launch_template") as mock_delete_lt: + with ( + patch.object(handler, "_delete_orb_launch_template") as mock_delete_lt, + patch.object( + handler._release_manager, + "_fleet_has_no_remaining_instances", + return_value=False, + ), + ): handler._release_hosts_for_single_spot_fleet( "sfr-123", ["i-1"], @@ -552,13 +564,17 @@ def test_request_type_deletes_fleet_with_terminate_false_and_calls_cleanup(self) ) cleanup_fn.assert_called_once_with("ec2_fleet", "req-ec2-abc") - def test_instant_type_no_fleet_delete_but_calls_cleanup(self): + def test_instant_type_calls_delete_fleets_and_cleanup(self): + # Instant fleets are NOT guaranteed to be auto-deleted by AWS; ORB must + # issue an explicit delete_fleets so the fleet is cleaned up promptly. cleanup_fn = MagicMock() mgr, aws_client, _ = _make_ec2_fleet_release_manager(cleanup_fn) with patch.object(mgr, "_delete_fleet") as mock_delete: mgr.release("fleet-3", ["i-1"], self._fleet_details("instant", 1)) mock_delete.assert_not_called() - aws_client.ec2_client.delete_fleets.assert_not_called() + aws_client.ec2_client.delete_fleets.assert_called_once_with( + FleetIds=["fleet-3"], TerminateInstances=True + ) cleanup_fn.assert_called_once_with("ec2_fleet", "req-ec2-abc") def test_request_type_missing_request_id_tag_skips_cleanup_gracefully(self): @@ -806,16 +822,16 @@ def test_empty_asg_details_logs_warning(self): mgr.release_instances("asg-no-details", ["i-1"], {}) cast(MagicMock, mgr._logger.warning).assert_called() - def test_empty_asg_details_does_not_call_detach(self): + def test_empty_asg_details_does_not_call_terminate_in_asg(self): mgr, aws_client, _cleanup_fn = _make_asg_capacity_manager() aws_client.autoscaling_client.describe_auto_scaling_groups.return_value = ( self._empty_describe_response() ) mgr.release_instances("asg-no-details", ["i-1"], {}) - aws_client.autoscaling_client.detach_instances.assert_not_called() + aws_client.autoscaling_client.terminate_instance_in_auto_scaling_group.assert_not_called() def test_empty_asg_details_retry_succeeds_continues_normal_path(self): - """When the retry describe returns a valid ASG, normal detach+terminate path runs.""" + """When the retry describe returns a valid ASG, terminate_instance_in_auto_scaling_group is called.""" cleanup_fn = MagicMock() mgr, aws_client, _ = _make_asg_capacity_manager(cleanup_fn) aws_client.autoscaling_client.describe_auto_scaling_groups.return_value = { @@ -824,8 +840,9 @@ def test_empty_asg_details_retry_succeeds_continues_normal_path(self): delete_fn = MagicMock() mgr.set_delete_asg_fn(delete_fn) mgr.release_instances("asg-retry-ok", ["i-1"], {}) - aws_client.autoscaling_client.detach_instances.assert_called_once() - cast(MagicMock, mgr._aws_ops.terminate_instances_with_fallback).assert_called_once() + aws_client.autoscaling_client.terminate_instance_in_auto_scaling_group.assert_called_once_with( + InstanceId="i-1", ShouldDecrementDesiredCapacity=True + ) def test_valid_asg_details_at_zero_capacity_calls_delete_and_cleanup(self): cleanup_fn = MagicMock() diff --git a/tests/providers/aws/unit/infrastructure/handlers/test_ec2_fleet_handler.py b/tests/providers/aws/unit/infrastructure/handlers/test_ec2_fleet_handler.py new file mode 100644 index 000000000..91370ce5c --- /dev/null +++ b/tests/providers/aws/unit/infrastructure/handlers/test_ec2_fleet_handler.py @@ -0,0 +1,339 @@ +"""Unit tests for EC2FleetHandler.check_hosts_status and ProviderFulfilment.""" + +from unittest.mock import MagicMock, patch + +import pytest +from botocore.exceptions import ClientError + +from orb.domain.base.provider_fulfilment import ( + CheckHostsStatusResult, + FulfilmentState, + ProviderFulfilment, +) +from orb.providers.aws.exceptions.aws_exceptions import AWSInfrastructureError +from orb.providers.aws.infrastructure.handlers.ec2_fleet.handler import EC2FleetHandler + + +def _make_handler(): + aws_client = MagicMock() + logger = MagicMock() + aws_ops = MagicMock() + launch_template_manager = MagicMock() + handler = EC2FleetHandler(aws_client, logger, aws_ops, launch_template_manager) + handler._machine_adapter = None + return handler + + +def _make_request(resource_ids, metadata=None, requested_count=2): + request = MagicMock() + request.request_id = "req-ec2fleet-123" + request.resource_ids = resource_ids + request.metadata = metadata or {} + request.requested_count = requested_count + return request + + +def _make_client_error(code="InternalError"): + return ClientError({"Error": {"Code": code, "Message": "boom"}}, "DescribeFleets") + + +def _inst(iid, resource_id="fleet-test", status="running"): + return { + "instance_id": iid, + "resource_id": resource_id, + "status": status, + "private_ip": "10.0.0.1", + "public_ip": None, + "launch_time": None, + "instance_type": "t3.medium", + "image_id": "ami-123", + "subnet_id": None, + "security_group_ids": [], + "vpc_id": None, + } + + +def _formatted_instances(instance_ids, resource_id="fleet-test"): + return [_inst(iid, resource_id) for iid in instance_ids] + + +def _fleet_result(instance_ids, resource_id="fleet-test", state: FulfilmentState = "fulfilled"): + """Build a CheckHostsStatusResult for mocking _check_single_fleet_status.""" + return CheckHostsStatusResult( + instances=_formatted_instances(instance_ids, resource_id), + fulfilment=ProviderFulfilment(state=state, message="test"), + ) + + +class TestEC2FleetHandlerCheckHostsStatus: + def test_check_hosts_status_returns_check_hosts_status_result(self): + """check_hosts_status returns CheckHostsStatusResult (not a plain list).""" + handler = _make_handler() + request = _make_request(["fleet-111"], metadata={"fleet_type": "maintain"}) + with patch.object( + handler, "_check_single_fleet_status", return_value=_fleet_result(["i-aaa"]) + ): + result = handler.check_hosts_status(request) + assert isinstance(result, CheckHostsStatusResult) + + def test_check_hosts_status_all_running(self): + """All instances running → instances present and fulfilled.""" + handler = _make_handler() + request = _make_request(["fleet-111"], metadata={"fleet_type": "maintain"}) + instance_ids = ["i-aaa", "i-bbb"] + with patch.object( + handler, + "_check_single_fleet_status", + return_value=_fleet_result(instance_ids, "fleet-111"), + ): + result = handler.check_hosts_status(request) + + assert len(result.instances) == 2 + returned_ids = {r["instance_id"] for r in result.instances} + assert returned_ids == set(instance_ids) + + def test_check_hosts_status_mixed_states(self): + """Active instances returned as reported.""" + handler = _make_handler() + request = _make_request(["fleet-222"], metadata={"fleet_type": "maintain"}) + with patch.object( + handler, + "_check_single_fleet_status", + return_value=_fleet_result(["i-running1"], "fleet-222"), + ): + result = handler.check_hosts_status(request) + + assert len(result.instances) == 1 + assert result.instances[0]["instance_id"] == "i-running1" + + def test_check_hosts_status_fleet_not_found(self): + """_check_single_fleet_status raises → exception logged; result has empty instances.""" + handler = _make_handler() + request = _make_request(["fleet-missing"], metadata={"fleet_type": "maintain"}) + with patch.object( + handler, + "_check_single_fleet_status", + side_effect=AWSInfrastructureError("Fleet not found"), + ): + result = handler.check_hosts_status(request) + + assert result.instances == [] + assert result.fulfilment.state == "in_progress" + + def test_check_hosts_status_multiple_resource_ids(self): + """Request has 2 fleet IDs → checks both, aggregates instances.""" + handler = _make_handler() + request = _make_request(["fleet-A", "fleet-B"], metadata={"fleet_type": "maintain"}) + ids_a = ["i-a1", "i-a2"] + ids_b = ["i-b1"] + + def single_fleet_side_effect(fleet_id, req): + if fleet_id == "fleet-A": + return _fleet_result(ids_a, "fleet-A") + return _fleet_result(ids_b, "fleet-B") + + with patch.object( + handler, "_check_single_fleet_status", side_effect=single_fleet_side_effect + ): + result = handler.check_hosts_status(request) + + assert len(result.instances) == 3 + returned_ids = {r["instance_id"] for r in result.instances} + assert returned_ids == {"i-a1", "i-a2", "i-b1"} + + def test_check_hosts_status_aws_error(self): + """ClientError inside per-fleet loop → logged and skipped; empty instances.""" + handler = _make_handler() + request = _make_request(["fleet-err"], metadata={"fleet_type": "maintain"}) + with patch.object( + handler, "_check_single_fleet_status", side_effect=_make_client_error("InternalError") + ): + result = handler.check_hosts_status(request) + + assert result.instances == [] + + def test_check_hosts_status_no_resource_ids(self): + """Empty resource_ids → raises AWSInfrastructureError immediately.""" + handler = _make_handler() + request = _make_request([]) + with pytest.raises(AWSInfrastructureError): + handler.check_hosts_status(request) + + def test_check_hosts_status_returns_correct_count(self): + """Verify instance count in result matches instances returned.""" + handler = _make_handler() + request = _make_request(["fleet-cnt"], metadata={"fleet_type": "maintain"}) + with patch.object( + handler, "_check_single_fleet_status", return_value=_fleet_result(["i-1", "i-2", "i-3"]) + ): + result = handler.check_hosts_status(request) + + assert len(result.instances) == 3 + + def test_check_hosts_status_instant_fleet_no_active_instances(self): + """Instant fleet with no instances → empty instances, in_progress.""" + handler = _make_handler() + request = _make_request( + ["fleet-instant"], metadata={"fleet_type": "instant", "instance_ids": []} + ) + empty_result = CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment(state="in_progress", message="waiting"), + ) + with patch.object(handler, "_check_single_fleet_status", return_value=empty_result): + result = handler.check_hosts_status(request) + + assert result.instances == [] + assert result.fulfilment.state == "in_progress" + + +class TestEC2FleetFulfilment: + """Unit tests for EC2Fleet fulfilment computation.""" + + def _handler(self): + return _make_handler() + + def _inst_dict(self, status): + return {"instance_id": "i-x", "status": status} + + def test_maintain_fleet_fulfilled_when_capacity_met_and_no_pending(self): + """Maintain: FulfilledCapacity >= TargetCapacity AND pending==0 → fulfilled.""" + from orb.providers.aws.domain.template.aws_template_aggregate import AWSFleetType + + h = self._handler() + instances = [self._inst_dict("running"), self._inst_dict("running")] + f = h._compute_ec2fleet_fulfilment( + fleet_type=AWSFleetType.MAINTAIN, + instances=instances, + target_capacity=4, + fulfilled_capacity=4.0, + requested_count=4, + ) + assert f.state == "fulfilled" + assert f.running_count == 2 + assert f.fulfilled_units == 4 + + def test_maintain_fleet_in_progress_when_pending_exists(self): + """Maintain: capacity met but pending instances → in_progress.""" + from orb.providers.aws.domain.template.aws_template_aggregate import AWSFleetType + + h = self._handler() + instances = [self._inst_dict("running"), self._inst_dict("pending")] + f = h._compute_ec2fleet_fulfilment( + fleet_type=AWSFleetType.MAINTAIN, + instances=instances, + target_capacity=4, + fulfilled_capacity=4.0, + requested_count=4, + ) + assert f.state == "in_progress" + + def test_maintain_fleet_in_progress_when_capacity_below_target(self): + """Maintain: FulfilledCapacity < TargetCapacity → in_progress.""" + from orb.providers.aws.domain.template.aws_template_aggregate import AWSFleetType + + h = self._handler() + instances = [self._inst_dict("running")] + f = h._compute_ec2fleet_fulfilment( + fleet_type=AWSFleetType.MAINTAIN, + instances=instances, + target_capacity=4, + fulfilled_capacity=2.0, + requested_count=4, + ) + assert f.state == "in_progress" + assert f.fulfilled_units == 2 + + def test_instant_fleet_fulfilled_when_count_meets_requested(self): + """Instant: running_count >= requested_count → fulfilled.""" + from orb.providers.aws.domain.template.aws_template_aggregate import AWSFleetType + + h = self._handler() + instances = [self._inst_dict("running"), self._inst_dict("running")] + f = h._compute_ec2fleet_fulfilment( + fleet_type=AWSFleetType.INSTANT, + instances=instances, + target_capacity=2, + fulfilled_capacity=2.0, + requested_count=2, + ) + assert f.state == "fulfilled" + + def test_instant_fleet_partial_when_fewer_running_no_pending(self): + """Instant: running < requested, no pending → partial (final).""" + from orb.providers.aws.domain.template.aws_template_aggregate import AWSFleetType + + h = self._handler() + instances = [self._inst_dict("running")] + f = h._compute_ec2fleet_fulfilment( + fleet_type=AWSFleetType.INSTANT, + instances=instances, + target_capacity=4, + fulfilled_capacity=1.0, + requested_count=4, + ) + assert f.state == "partial" + + def test_instant_fleet_in_progress_when_no_instances_yet(self): + """Instant: no instances yet → in_progress.""" + from orb.providers.aws.domain.template.aws_template_aggregate import AWSFleetType + + h = self._handler() + f = h._compute_ec2fleet_fulfilment( + fleet_type=AWSFleetType.INSTANT, + instances=[], + target_capacity=2, + fulfilled_capacity=0.0, + requested_count=2, + ) + assert f.state == "in_progress" + + +class TestEC2FleetHandlerNameTag: + def test_fleet_config_instance_tag_uses_config_prefix(self): + """Instance Name tag in EC2Fleet config uses config_port prefix (instant fleet only).""" + from orb.providers.aws.domain.template.aws_template_aggregate import AWSFleetType + + aws_client = MagicMock() + logger = MagicMock() + aws_ops = MagicMock() + launch_template_manager = MagicMock() + config_port = MagicMock() + config_port.get_resource_prefix.side_effect = lambda rt: ( + "pfx-" if rt == "fleet" else "inst-" + ) + + handler = EC2FleetHandler( + aws_client, logger, aws_ops, launch_template_manager, config_port=config_port + ) + + template = MagicMock() + template.fleet_type = AWSFleetType.INSTANT + template.tags = {} + template.price_type = "ondemand" + template.allocation_strategy = None + template.max_price = None + template.machine_types = {"m5.large": 1} + template.subnet_ids = ["subnet-abc"] + template.template_id = "tmpl-ec2" + template.percent_on_demand = None + template.context = None + + request = MagicMock() + request.request_id = "req-ec2-001" + request.requested_count = 2 + + fleet_config = handler._create_fleet_config(template, request, "lt-xyz", "$Default") + + instance_ts = next( + ( + ts + for ts in fleet_config.get("TagSpecifications", []) + if ts["ResourceType"] == "instance" + ), + None, + ) + assert instance_ts is not None, "No instance TagSpecification found (instant fleet)" + name_tag = next((t for t in instance_ts["Tags"] if t["Key"] == "Name"), None) + assert name_tag is not None, "No Name tag in instance TagSpecification" + assert "req-ec2-001" in name_tag["Value"] diff --git a/tests/providers/aws/unit/infrastructure/handlers/test_fleet_release_weighted_capacity.py b/tests/providers/aws/unit/infrastructure/handlers/test_fleet_release_weighted_capacity.py new file mode 100644 index 000000000..56510fd6a --- /dev/null +++ b/tests/providers/aws/unit/infrastructure/handlers/test_fleet_release_weighted_capacity.py @@ -0,0 +1,514 @@ +"""Unit tests for weighted-capacity arithmetic in EC2Fleet and SpotFleet release managers. + +Verifies that: +- compute_fleet_release_decision uses weighted_capacity_to_return (not instance count). +- EC2FleetReleaseManager._sum_weighted_capacity sums WeightedCapacity per InstanceType. +- SpotFleetReleaseManager._sum_weighted_capacity uses per-instance WeightedCapacity from + describe_spot_fleet_instances, with a fallback to the fleet spec weight-by-type map. +- The release() path passes the weighted sum to modify_fleet / modify_spot_fleet_request. +""" + +from typing import Any +from unittest.mock import MagicMock + +from orb.providers.aws.infrastructure.handlers.ec2_fleet.release_manager import ( + EC2FleetReleaseManager, +) +from orb.providers.aws.infrastructure.handlers.fleet_release_policy import ( + compute_fleet_release_decision, +) +from orb.providers.aws.infrastructure.handlers.spot_fleet.release_manager import ( + SpotFleetReleaseManager, +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_ec2_release_manager(ec2_client_mock: Any = None) -> EC2FleetReleaseManager: + aws_client = MagicMock() + if ec2_client_mock is not None: + aws_client.ec2_client = ec2_client_mock + logger = MagicMock() + aws_ops = MagicMock() + aws_ops.terminate_instances_with_fallback = MagicMock() + + def identity_retry(fn, operation_type="standard", **kwargs): + if callable(fn) and not kwargs: + return fn() + return fn(**kwargs) + + def identity_paginate(method, key, **kwargs): + return method(**kwargs).get(key, []) + + def identity_collect(method, key, **kwargs): + return method(**kwargs).get(key, []) + + return EC2FleetReleaseManager( + aws_client=aws_client, + aws_ops=aws_ops, + request_adapter=None, + config_port=None, + logger=logger, + retry_fn=identity_retry, + paginate_fn=identity_paginate, + collect_with_next_token_fn=identity_collect, + cleanup_on_zero_capacity_fn=MagicMock(), + ) + + +def _make_spot_release_manager(ec2_client_mock: Any = None) -> SpotFleetReleaseManager: + aws_client = MagicMock() + if ec2_client_mock is not None: + aws_client.ec2_client = ec2_client_mock + logger = MagicMock() + aws_ops = MagicMock() + aws_ops.terminate_instances_with_fallback = MagicMock() + + def identity_retry(fn, operation_type="standard", **kwargs): + if callable(fn) and not kwargs: + return fn() + return fn(**kwargs) + + mgr = SpotFleetReleaseManager( + aws_client=aws_client, + aws_ops=aws_ops, + request_adapter=None, + cleanup_on_zero_capacity_fn=MagicMock(), + logger=logger, + retry_fn=identity_retry, + ) + return mgr + + +def _fleet_details(fleet_type: str, total_capacity: int, overrides: list[dict]) -> dict: + """Build a minimal DescribeFleets entry with LaunchTemplateConfigs overrides.""" + return { + "Type": fleet_type, + "TargetCapacitySpecification": {"TotalTargetCapacity": total_capacity}, + "LaunchTemplateConfigs": [{"Overrides": overrides}], + "Tags": [{"Key": "orb:request-id", "Value": "req-test-001"}], + } + + +def _spot_fleet_config(fleet_type: str, target_capacity: int, launch_specs: list[dict]) -> dict: + """Build a minimal SpotFleetRequestConfig with LaunchSpecifications.""" + return { + "Type": fleet_type, + "TargetCapacity": target_capacity, + "OnDemandTargetCapacity": 0, + "LaunchSpecifications": launch_specs, + } + + +def _active_instances_response(instances: list[dict]) -> dict: + """Wrap a list of instance dicts into a describe_fleet_instances response.""" + return {"ActiveInstances": instances} + + +# --------------------------------------------------------------------------- +# fleet_release_policy unit tests +# --------------------------------------------------------------------------- + + +class TestComputeFleetReleaseDecision: + def test_maintain_partial_return_requires_capacity_reduction(self): + decision = compute_fleet_release_decision( + fleet_type="maintain", + current_capacity=10, + weighted_capacity_to_return=4, + ) + assert decision.requires_capacity_reduction is True + assert decision.has_fleet_record is True + assert decision.is_full_return is False + + def test_maintain_full_return_is_full(self): + decision = compute_fleet_release_decision( + fleet_type="maintain", + current_capacity=4, + weighted_capacity_to_return=4, + ) + assert decision.requires_capacity_reduction is True + assert decision.is_full_return is True + + def test_maintain_weighted_return_exceeds_capacity_clamps_to_full(self): + """If weighted sum > current (race / stale data), still produces is_full_return=True.""" + decision = compute_fleet_release_decision( + fleet_type="maintain", + current_capacity=3, + weighted_capacity_to_return=8, + ) + assert decision.is_full_return is True + + def test_request_fleet_no_capacity_reduction_required(self): + decision = compute_fleet_release_decision( + fleet_type="request", + current_capacity=5, + weighted_capacity_to_return=2, + ) + assert decision.requires_capacity_reduction is False + assert decision.has_fleet_record is True + assert decision.is_full_return is False + + def test_instant_fleet_no_fleet_record(self): + decision = compute_fleet_release_decision( + fleet_type="instant", + current_capacity=2, + weighted_capacity_to_return=2, + ) + assert decision.requires_capacity_reduction is False + assert decision.has_fleet_record is False + assert decision.is_full_return is True + + def test_weighted_capacity_used_not_instance_count(self): + """Two instances with WeightedCapacity=4 each: subtract 8, not 2.""" + decision = compute_fleet_release_decision( + fleet_type="maintain", + current_capacity=8, + weighted_capacity_to_return=8, + ) + assert decision.is_full_return is True + + # With old (instance-count) arithmetic this would be False (8 - 2 == 6 != 0). + decision_old_style = compute_fleet_release_decision( + fleet_type="maintain", + current_capacity=8, + weighted_capacity_to_return=2, + ) + assert decision_old_style.is_full_return is False + + +# --------------------------------------------------------------------------- +# EC2FleetReleaseManager._sum_weighted_capacity +# --------------------------------------------------------------------------- + + +class TestEC2FleetSumWeightedCapacity: + def _make_manager(self, active_instances: list[dict]) -> EC2FleetReleaseManager: + ec2 = MagicMock() + ec2.describe_fleet_instances.return_value = {"ActiveInstances": active_instances} + return _make_ec2_release_manager(ec2_client_mock=ec2) + + def test_uniform_weights_returns_sum(self): + """Two m5.large instances each with WeightedCapacity=2 → returns 4.""" + active = [ + {"InstanceId": "i-aaa", "InstanceType": "m5.large"}, + {"InstanceId": "i-bbb", "InstanceType": "m5.large"}, + ] + overrides = [{"InstanceType": "m5.large", "WeightedCapacity": 2}] + mgr = self._make_manager(active) + details = _fleet_details("maintain", 4, overrides) + result = mgr._sum_weighted_capacity("fleet-111", details, ["i-aaa", "i-bbb"]) + assert result == 4 + + def test_mixed_weights_sums_correctly(self): + """One c5.xlarge (weight 4) + one m5.large (weight 2) → 6.""" + active = [ + {"InstanceId": "i-aaa", "InstanceType": "c5.xlarge"}, + {"InstanceId": "i-bbb", "InstanceType": "m5.large"}, + ] + overrides = [ + {"InstanceType": "c5.xlarge", "WeightedCapacity": 4}, + {"InstanceType": "m5.large", "WeightedCapacity": 2}, + ] + mgr = self._make_manager(active) + details = _fleet_details("maintain", 6, overrides) + result = mgr._sum_weighted_capacity("fleet-222", details, ["i-aaa", "i-bbb"]) + assert result == 6 + + def test_instance_not_in_active_defaults_to_1(self): + """Instance absent from ActiveInstances (already terminated) defaults to weight 1.""" + active: list[dict] = [] # already gone + overrides = [{"InstanceType": "c5.xlarge", "WeightedCapacity": 4}] + mgr = self._make_manager(active) + details = _fleet_details("maintain", 4, overrides) + result = mgr._sum_weighted_capacity("fleet-333", details, ["i-gone"]) + assert result == 1 + + def test_no_overrides_defaults_to_instance_count(self): + """Fleet with no WeightedCapacity overrides → sum equals instance count.""" + active = [ + {"InstanceId": "i-aaa", "InstanceType": "t3.medium"}, + {"InstanceId": "i-bbb", "InstanceType": "t3.medium"}, + ] + mgr = self._make_manager(active) + details = _fleet_details("maintain", 2, overrides=[]) + result = mgr._sum_weighted_capacity("fleet-444", details, ["i-aaa", "i-bbb"]) + assert result == 2 + + def test_describe_fleet_instances_error_defaults_all_to_1(self): + """If describe_fleet_instances raises, all instances default to weight 1.""" + ec2 = MagicMock() + ec2.describe_fleet_instances.side_effect = Exception("throttled") + mgr = _make_ec2_release_manager(ec2_client_mock=ec2) + overrides = [{"InstanceType": "c5.xlarge", "WeightedCapacity": 4}] + details = _fleet_details("maintain", 8, overrides) + result = mgr._sum_weighted_capacity("fleet-555", details, ["i-aaa", "i-bbb"]) + assert result == 2 # 2 instances × default weight 1 + + def test_minimum_result_is_1(self): + """Even with an empty instance_ids list the result is at least 1.""" + ec2 = MagicMock() + ec2.describe_fleet_instances.return_value = {"ActiveInstances": []} + mgr = _make_ec2_release_manager(ec2_client_mock=ec2) + details = _fleet_details("maintain", 4, []) + # Pass a single instance that defaults to weight 1 (empty active list). + result = mgr._sum_weighted_capacity("fleet-666", details, ["i-only"]) + assert result >= 1 + + +# --------------------------------------------------------------------------- +# EC2FleetReleaseManager.release() — weighted path end-to-end +# --------------------------------------------------------------------------- + + +class TestEC2FleetReleaseWeighted: + def _setup(self, active_instances: list[dict], total_capacity: int, overrides: list[dict]): + ec2 = MagicMock() + ec2.describe_fleet_instances.return_value = {"ActiveInstances": active_instances} + # After our instances are terminated, remaining fleet is also empty → delete + ec2.describe_fleet_instances.side_effect = [ + {"ActiveInstances": active_instances}, # first call: _sum_weighted_capacity + {"ActiveInstances": []}, # second call: _fleet_has_no_remaining_instances + ] + ec2.modify_fleet.return_value = {} + ec2.delete_fleets.return_value = { + "SuccessfulFleetDeletions": [], + "UnsuccessfulFleetDeletions": [], + } + mgr = _make_ec2_release_manager(ec2_client_mock=ec2) + fleet_details = _fleet_details("maintain", total_capacity, overrides) + fleet_details["Tags"] = [{"Key": "orb:request-id", "Value": "req-weighted-001"}] + return mgr, ec2, fleet_details + + def test_modify_fleet_called_with_weighted_new_capacity(self): + """modify_fleet receives current_capacity - sum(weights), not current_capacity - count.""" + active = [ + {"InstanceId": "i-aaa", "InstanceType": "c5.xlarge"}, + {"InstanceId": "i-bbb", "InstanceType": "c5.xlarge"}, + ] + # Each c5.xlarge has WeightedCapacity=4; returning 2 → subtract 8. + overrides = [{"InstanceType": "c5.xlarge", "WeightedCapacity": 4}] + mgr, ec2, fleet_details = self._setup(active, total_capacity=8, overrides=overrides) + + mgr.release( + fleet_id="fleet-weighted", + instance_ids=["i-aaa", "i-bbb"], + fleet_details=fleet_details, + ) + + ec2.modify_fleet.assert_called_once_with( + FleetId="fleet-weighted", + TargetCapacitySpecification={"TotalTargetCapacity": 0}, + ) + + def test_maintain_fleet_partial_return_uses_weighted_sum(self): + """Partial return: 1 of 2 c5.xlarge (weight=4) → new capacity = 8 - 4 = 4.""" + active = [ + {"InstanceId": "i-aaa", "InstanceType": "c5.xlarge"}, + {"InstanceId": "i-bbb", "InstanceType": "c5.xlarge"}, + ] + overrides = [{"InstanceType": "c5.xlarge", "WeightedCapacity": 4}] + ec2 = MagicMock() + # First describe call returns both instances (for _sum_weighted_capacity). + # Second call returns i-bbb still active (partial return → fleet not deleted). + ec2.describe_fleet_instances.side_effect = [ + {"ActiveInstances": active}, + {"ActiveInstances": [{"InstanceId": "i-bbb", "InstanceType": "c5.xlarge"}]}, + ] + ec2.modify_fleet.return_value = {} + mgr = _make_ec2_release_manager(ec2_client_mock=ec2) + fleet_details = _fleet_details("maintain", 8, overrides) + + mgr.release( + fleet_id="fleet-partial", + instance_ids=["i-aaa"], + fleet_details=fleet_details, + ) + + ec2.modify_fleet.assert_called_once_with( + FleetId="fleet-partial", + TargetCapacitySpecification={"TotalTargetCapacity": 4}, + ) + + +# --------------------------------------------------------------------------- +# SpotFleetReleaseManager._sum_weighted_capacity +# --------------------------------------------------------------------------- + + +class TestSpotFleetSumWeightedCapacity: + def _make_manager(self, active_response: dict) -> SpotFleetReleaseManager: + ec2 = MagicMock() + ec2.describe_spot_fleet_instances.return_value = active_response + return _make_spot_release_manager(ec2_client_mock=ec2) + + def test_per_instance_weight_from_describe(self): + """Uses WeightedCapacity directly from ActiveInstances response.""" + active_response = { + "ActiveInstances": [ + {"InstanceId": "i-s1", "InstanceType": "c5.xlarge", "WeightedCapacity": "4"}, + {"InstanceId": "i-s2", "InstanceType": "c5.xlarge", "WeightedCapacity": "4"}, + ] + } + fleet_config = _spot_fleet_config("maintain", 8, []) + mgr = self._make_manager(active_response) + result = mgr._sum_weighted_capacity("sfr-111", fleet_config, ["i-s1", "i-s2"]) + assert result == 8 + + def test_mixed_per_instance_weights(self): + """Different weights per instance are summed correctly.""" + active_response = { + "ActiveInstances": [ + {"InstanceId": "i-s1", "InstanceType": "c5.xlarge", "WeightedCapacity": "4"}, + {"InstanceId": "i-s2", "InstanceType": "m5.large", "WeightedCapacity": "2"}, + ] + } + fleet_config = _spot_fleet_config("maintain", 6, []) + mgr = self._make_manager(active_response) + result = mgr._sum_weighted_capacity("sfr-222", fleet_config, ["i-s1", "i-s2"]) + assert result == 6 + + def test_fallback_to_launch_spec_weight_when_instance_missing(self): + """Instance absent from ActiveInstances: falls back to LaunchSpecifications weight.""" + active_response = {"ActiveInstances": []} + fleet_config = _spot_fleet_config( + "maintain", 4, [{"InstanceType": "c5.xlarge", "WeightedCapacity": 4}] + ) + # The instance is in launch specs but gone from active → use type weight from spec. + # However since it's not in instance_type_by_id either, it defaults to 1. + # The per-instance map is empty; type map has c5.xlarge=4 but we can't resolve + # the instance to a type. Default must be 1. + mgr = self._make_manager(active_response) + result = mgr._sum_weighted_capacity("sfr-333", fleet_config, ["i-gone"]) + assert result == 1 + + def test_launch_spec_weight_used_when_instance_has_no_weighted_capacity_field(self): + """Instance in ActiveInstances but WeightedCapacity field absent → type fallback.""" + active_response = { + "ActiveInstances": [ + {"InstanceId": "i-s1", "InstanceType": "c5.xlarge"}, # no WeightedCapacity + ] + } + fleet_config = _spot_fleet_config( + "maintain", 4, [{"InstanceType": "c5.xlarge", "WeightedCapacity": 4}] + ) + mgr = self._make_manager(active_response) + result = mgr._sum_weighted_capacity("sfr-444", fleet_config, ["i-s1"]) + assert result == 4 + + def test_no_weights_anywhere_defaults_to_instance_count(self): + """No weights in describe or fleet config → sum equals instance count.""" + active_response = { + "ActiveInstances": [ + {"InstanceId": "i-s1", "InstanceType": "t3.medium"}, + {"InstanceId": "i-s2", "InstanceType": "t3.medium"}, + ] + } + fleet_config = _spot_fleet_config("maintain", 2, []) + mgr = self._make_manager(active_response) + result = mgr._sum_weighted_capacity("sfr-555", fleet_config, ["i-s1", "i-s2"]) + assert result == 2 + + def test_describe_error_defaults_all_to_1(self): + """If describe_spot_fleet_instances raises, all instances default to weight 1.""" + ec2 = MagicMock() + ec2.describe_spot_fleet_instances.side_effect = Exception("throttled") + mgr = _make_spot_release_manager(ec2_client_mock=ec2) + fleet_config = _spot_fleet_config( + "maintain", 8, [{"InstanceType": "c5.xlarge", "WeightedCapacity": 4}] + ) + result = mgr._sum_weighted_capacity("sfr-666", fleet_config, ["i-s1", "i-s2"]) + assert result == 2 # 2 instances × default weight 1 + + +# --------------------------------------------------------------------------- +# SpotFleetReleaseManager.release() — weighted path end-to-end +# --------------------------------------------------------------------------- + + +class TestSpotFleetReleaseWeighted: + def _make_fleet_details(self, fleet_type: str, target: int, specs: list[dict]) -> dict: + return { + "SpotFleetRequestConfig": { + "Type": fleet_type, + "TargetCapacity": target, + "OnDemandTargetCapacity": 0, + "LaunchSpecifications": specs, + "TagSpecifications": [], + }, + "Tags": [{"Key": "orb:request-id", "Value": "req-spot-weighted"}], + } + + def test_modify_spot_fleet_called_with_weighted_new_capacity(self): + """modify_spot_fleet_request receives target - sum(weights), not target - count.""" + active_response = { + "ActiveInstances": [ + {"InstanceId": "i-s1", "InstanceType": "c5.xlarge", "WeightedCapacity": "4"}, + {"InstanceId": "i-s2", "InstanceType": "c5.xlarge", "WeightedCapacity": "4"}, + ] + } + ec2 = MagicMock() + # First call: _sum_weighted_capacity; second call: _fleet_has_no_remaining_instances + ec2.describe_spot_fleet_instances.side_effect = [ + active_response, + {"ActiveInstances": []}, + ] + ec2.modify_spot_fleet_request.return_value = {} + ec2.cancel_spot_fleet_requests.return_value = { + "SuccessfulFleetCancellations": [], + "UnsuccessfulFleetCancellations": [], + } + mgr = _make_spot_release_manager(ec2_client_mock=ec2) + fleet_details = self._make_fleet_details( + "maintain", 8, [{"InstanceType": "c5.xlarge", "WeightedCapacity": 4}] + ) + + mgr.release( + fleet_id="sfr-weighted", + instance_ids=["i-s1", "i-s2"], + fleet_details=fleet_details, + ) + + ec2.modify_spot_fleet_request.assert_called_once_with( + SpotFleetRequestId="sfr-weighted", + TargetCapacity=0, + OnDemandTargetCapacity=0, + ) + + def test_partial_spot_release_uses_weighted_sum(self): + """Partial return: 1 of 2 c5.xlarge (weight=4) → new capacity = 8 - 4 = 4.""" + active_response_sum = { + "ActiveInstances": [ + {"InstanceId": "i-s1", "InstanceType": "c5.xlarge", "WeightedCapacity": "4"}, + {"InstanceId": "i-s2", "InstanceType": "c5.xlarge", "WeightedCapacity": "4"}, + ] + } + active_response_remaining = { + "ActiveInstances": [ + {"InstanceId": "i-s2", "InstanceType": "c5.xlarge", "WeightedCapacity": "4"}, + ] + } + ec2 = MagicMock() + ec2.describe_spot_fleet_instances.side_effect = [ + active_response_sum, + active_response_remaining, + ] + ec2.modify_spot_fleet_request.return_value = {} + mgr = _make_spot_release_manager(ec2_client_mock=ec2) + fleet_details = self._make_fleet_details( + "maintain", 8, [{"InstanceType": "c5.xlarge", "WeightedCapacity": 4}] + ) + + mgr.release( + fleet_id="sfr-partial", + instance_ids=["i-s1"], + fleet_details=fleet_details, + ) + + ec2.modify_spot_fleet_request.assert_called_once_with( + SpotFleetRequestId="sfr-partial", + TargetCapacity=4, + OnDemandTargetCapacity=0, + ) diff --git a/tests/unit/providers/aws/infrastructure/handlers/test_run_instances_handler.py b/tests/providers/aws/unit/infrastructure/handlers/test_run_instances_handler.py similarity index 67% rename from tests/unit/providers/aws/infrastructure/handlers/test_run_instances_handler.py rename to tests/providers/aws/unit/infrastructure/handlers/test_run_instances_handler.py index 6724b261f..538ff8f83 100644 --- a/tests/unit/providers/aws/infrastructure/handlers/test_run_instances_handler.py +++ b/tests/providers/aws/unit/infrastructure/handlers/test_run_instances_handler.py @@ -1,4 +1,4 @@ -"""Unit tests for RunInstancesHandler.check_hosts_status.""" +"""Unit tests for RunInstancesHandler.check_hosts_status and ProviderFulfilment.""" from typing import Any from unittest.mock import MagicMock, patch @@ -6,6 +6,7 @@ import pytest from botocore.exceptions import ClientError +from orb.domain.base.provider_fulfilment import CheckHostsStatusResult from orb.providers.aws.exceptions.aws_exceptions import AWSInfrastructureError, AWSValidationError from orb.providers.aws.infrastructure.handlers.run_instances.handler import RunInstancesHandler from orb.providers.aws.infrastructure.launch_template.manager import LTNetworkingState @@ -21,13 +22,16 @@ def _make_handler() -> Any: return handler -def _make_request(resource_ids=None, instance_ids=None, provider_data=None, metadata=None): +def _make_request( + resource_ids=None, instance_ids=None, provider_data=None, metadata=None, requested_count=2 +): request = MagicMock() request.request_id = "req-ri-123" request.resource_ids = resource_ids or [] request.provider_api = "RunInstances" request.provider_data = provider_data if provider_data is not None else {} request.metadata = metadata if metadata is not None else {} + request.requested_count = requested_count return request @@ -56,29 +60,69 @@ def _formatted_instances(instance_ids, resource_id="r-test"): class TestRunInstancesHandlerCheckHostsStatus: - def test_check_hosts_status_all_running(self): - """All running instances → returns all.""" + def test_check_hosts_status_returns_check_hosts_status_result(self): + """check_hosts_status returns CheckHostsStatusResult (not a plain list).""" handler = _make_handler() instance_ids = ["i-run1", "i-run2"] request = _make_request( resource_ids=["r-res1"], provider_data={"instance_ids": instance_ids, "reservation_id": "r-res1"}, + requested_count=2, ) + with patch.object( + handler, + "_get_instance_details", + return_value=_formatted_instances(instance_ids, "r-res1"), + ): + with patch.object( + handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts + ): + result = handler.check_hosts_status(request) + assert isinstance(result, CheckHostsStatusResult) + + def test_check_hosts_status_all_running_fulfilled(self): + """All running instances → fulfilment state == 'fulfilled'.""" + handler = _make_handler() + instance_ids = ["i-run1", "i-run2"] + request = _make_request( + resource_ids=["r-res1"], + provider_data={"instance_ids": instance_ids, "reservation_id": "r-res1"}, + requested_count=2, + ) + with patch.object( + handler, + "_get_instance_details", + return_value=_formatted_instances(instance_ids, "r-res1"), + ): + with patch.object( + handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts + ): + result = handler.check_hosts_status(request) + assert len(result.instances) == 2 + assert result.fulfilment.state == "fulfilled" + assert result.fulfilment.running_count == 2 + + def test_check_hosts_status_all_running_instances_ids(self): + """All running instances → instance IDs present.""" + handler = _make_handler() + instance_ids = ["i-run1", "i-run2"] + request = _make_request( + resource_ids=["r-res1"], + provider_data={"instance_ids": instance_ids, "reservation_id": "r-res1"}, + requested_count=2, + ) with patch.object( handler, "_get_instance_details", return_value=_formatted_instances(instance_ids, "r-res1"), ): with patch.object( - handler, - "_format_instance_data", - side_effect=lambda insts, rid, api_val: insts, + handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts ): result = handler.check_hosts_status(request) - assert len(result) == 2 - returned_ids = {r["instance_id"] for r in result} + returned_ids = {r["instance_id"] for r in result.instances} assert returned_ids == set(instance_ids) def test_check_hosts_status_mixed_states(self): @@ -88,31 +132,31 @@ def test_check_hosts_status_mixed_states(self): request = _make_request( resource_ids=["r-mixed"], provider_data={"instance_ids": instance_ids, "reservation_id": "r-mixed"}, + requested_count=3, ) - with patch.object( handler, "_get_instance_details", return_value=_formatted_instances(instance_ids, "r-mixed"), ): with patch.object( - handler, - "_format_instance_data", - side_effect=lambda insts, rid, api_val: insts, + handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts ): result = handler.check_hosts_status(request) - assert len(result) == 3 + assert len(result.instances) == 3 def test_check_hosts_status_empty_resource_ids(self): - """No resource_ids and no instance_ids → returns [].""" + """No resource_ids and no instance_ids → returns in_progress fulfilment.""" handler = _make_handler() request = _make_request(resource_ids=[], provider_data={}, metadata={}) with patch.object(handler, "_get_instance_details") as mock_get: result = handler.check_hosts_status(request) - assert result == [] + assert isinstance(result, CheckHostsStatusResult) + assert result.instances == [] + assert result.fulfilment.state == "in_progress" mock_get.assert_not_called() def test_check_hosts_status_aws_error(self): @@ -123,7 +167,6 @@ def test_check_hosts_status_aws_error(self): resource_ids=["r-err"], provider_data={"instance_ids": instance_ids, "reservation_id": "r-err"}, ) - with patch.object( handler, "_get_instance_details", side_effect=_make_client_error("InternalError") ): @@ -138,44 +181,40 @@ def test_check_hosts_status_falls_back_to_metadata_instance_ids(self): resource_ids=["r-meta"], provider_data={}, metadata={"instance_ids": instance_ids}, + requested_count=2, ) - with patch.object( handler, "_get_instance_details", return_value=_formatted_instances(instance_ids, "r-meta"), ): with patch.object( - handler, - "_format_instance_data", - side_effect=lambda insts, rid, api_val: insts, + handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts ): result = handler.check_hosts_status(request) - assert len(result) == 2 + assert len(result.instances) == 2 def test_check_hosts_status_returns_correct_count(self): - """Verify count matches instances returned.""" + """Verify instance count in result matches instances returned.""" handler = _make_handler() instance_ids = ["i-r1", "i-r2", "i-r3", "i-r4"] request = _make_request( resource_ids=["r-cnt"], provider_data={"instance_ids": instance_ids, "reservation_id": "r-cnt"}, + requested_count=4, ) - with patch.object( handler, "_get_instance_details", return_value=_formatted_instances(instance_ids, "r-cnt"), ): with patch.object( - handler, - "_format_instance_data", - side_effect=lambda insts, rid, api_val: insts, + handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts ): result = handler.check_hosts_status(request) - assert len(result) == 4 + assert len(result.instances) == 4 def test_check_hosts_status_preserves_instance_ids(self): """Instance IDs in result match input.""" @@ -184,61 +223,98 @@ def test_check_hosts_status_preserves_instance_ids(self): request = _make_request( resource_ids=["r-ids"], provider_data={"instance_ids": instance_ids, "reservation_id": "r-ids"}, + requested_count=2, ) - with patch.object( handler, "_get_instance_details", return_value=_formatted_instances(instance_ids, "r-ids"), ): with patch.object( - handler, - "_format_instance_data", - side_effect=lambda insts, rid, api_val: insts, + handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts ): result = handler.check_hosts_status(request) - returned_ids = {r["instance_id"] for r in result} + returned_ids = {r["instance_id"] for r in result.instances} assert returned_ids == set(instance_ids) def test_check_hosts_status_state_filtering_is_strict(self): """Only instances returned by _get_instance_details appear in result.""" handler = _make_handler() launched_ids = ["i-strict1", "i-strict2"] - # Only one instance returned (the other was terminated/not found) returned_by_aws = _formatted_instances(["i-strict1"], "r-strict") request = _make_request( resource_ids=["r-strict"], provider_data={"instance_ids": launched_ids, "reservation_id": "r-strict"}, + requested_count=2, ) - with patch.object(handler, "_get_instance_details", return_value=returned_by_aws): with patch.object( - handler, - "_format_instance_data", - side_effect=lambda insts, rid, api_val: insts, + handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts ): result = handler.check_hosts_status(request) - assert len(result) == 1 - assert result[0]["instance_id"] == "i-strict1" + assert len(result.instances) == 1 + assert result.instances[0]["instance_id"] == "i-strict1" def test_check_hosts_status_uses_resource_ids_when_no_instance_ids(self): """Falls back to _find_instances_by_resource_ids when no instance_ids anywhere.""" handler = _make_handler() - request = _make_request( - resource_ids=["r-fallback"], - provider_data={}, - metadata={}, - ) - - fallback_result = _formatted_instances(["i-fb1"], "r-fallback") + request = _make_request(resource_ids=["r-fallback"], provider_data={}, metadata={}) + from orb.domain.base.provider_fulfilment import ProviderFulfilment + fallback_result = CheckHostsStatusResult( + instances=_formatted_instances(["i-fb1"], "r-fallback"), + fulfilment=ProviderFulfilment(state="in_progress", message="test"), + ) with patch.object(handler, "_find_instances_by_resource_ids", return_value=fallback_result): result = handler.check_hosts_status(request) - assert len(result) == 1 - assert result[0]["instance_id"] == "i-fb1" + assert len(result.instances) == 1 + assert result.instances[0]["instance_id"] == "i-fb1" + + +class TestRunInstancesProviderFulfilment: + """Unit tests for RunInstances fulfilment computation.""" + + def _run(self, instances, requested_count): + handler = _make_handler() + return handler._compute_run_instances_fulfilment(instances, requested_count) + + def _inst(self, status): + return {"instance_id": "i-x", "status": status} + + def test_all_running_at_target_is_fulfilled(self): + instances = [self._inst("running"), self._inst("running")] + f = self._run(instances, 2) + assert f.state == "fulfilled" + assert f.running_count == 2 + + def test_running_exceeds_target_is_fulfilled(self): + instances = [self._inst("running")] * 3 + f = self._run(instances, 2) + assert f.state == "fulfilled" + + def test_pending_instances_is_in_progress(self): + instances = [self._inst("running"), self._inst("pending")] + f = self._run(instances, 2) + assert f.state == "in_progress" + assert f.pending_count == 1 + + def test_no_instances_is_in_progress(self): + f = self._run([], 2) + assert f.state == "in_progress" + + def test_some_running_below_target_is_in_progress(self): + instances = [self._inst("running")] + f = self._run(instances, 2) + # 1 running, no pending → partial + assert f.state == "partial" + + def test_all_failed_is_failed(self): + instances = [self._inst("failed"), self._inst("failed")] + f = self._run(instances, 2) + assert f.state == "failed" class TestRunInstancesHandlerMachineAdapterContext: @@ -250,6 +326,7 @@ def test_check_hosts_status_passes_context_to_get_instance_details(self): request = _make_request( resource_ids=[resource_id], provider_data={"instance_ids": instance_ids, "reservation_id": resource_id}, + requested_count=2, ) with patch.object(handler, "_get_instance_details", return_value=[]) as mock_details: diff --git a/tests/unit/providers/aws/infrastructure/handlers/test_spot_fleet_handler.py b/tests/providers/aws/unit/infrastructure/handlers/test_spot_fleet_handler.py similarity index 66% rename from tests/unit/providers/aws/infrastructure/handlers/test_spot_fleet_handler.py rename to tests/providers/aws/unit/infrastructure/handlers/test_spot_fleet_handler.py index 9acf8b23d..60411c44c 100644 --- a/tests/unit/providers/aws/infrastructure/handlers/test_spot_fleet_handler.py +++ b/tests/providers/aws/unit/infrastructure/handlers/test_spot_fleet_handler.py @@ -5,6 +5,11 @@ from botocore.exceptions import ClientError +from orb.domain.base.provider_fulfilment import ( + CheckHostsStatusResult, + FulfilmentState, + ProviderFulfilment, +) from orb.providers.aws.domain.template.aws_template_aggregate import AWSTemplate from orb.providers.aws.exceptions.aws_exceptions import AWSInfrastructureError from orb.providers.aws.infrastructure.handlers.spot_fleet.handler import SpotFleetHandler @@ -20,11 +25,12 @@ def _make_handler() -> Any: return handler -def _make_request(resource_ids): +def _make_request(resource_ids, requested_count: int = 3): request = MagicMock() request.request_id = "req-spot-123" request.resource_ids = resource_ids request.metadata = {} + request.requested_count = requested_count return request @@ -33,7 +39,7 @@ def _make_client_error(code="InternalError"): def _formatted_instances(instance_ids, resource_id="sfr-test"): - """Return already-formatted instance dicts (as _get_spot_fleet_instances returns them).""" + """Return already-formatted instance dicts.""" return [ { "instance_id": iid, @@ -52,170 +58,226 @@ def _formatted_instances(instance_ids, resource_id="sfr-test"): ] +def _fleet_status_result( + instance_ids, + resource_id: str = "sfr-test", + state: FulfilmentState = "fulfilled", + target_units: int | None = None, + fulfilled_units: int | None = None, +): + """Build a CheckHostsStatusResult for mocking _get_spot_fleet_status.""" + instances = _formatted_instances(instance_ids, resource_id) + n = len(instance_ids) + return CheckHostsStatusResult( + instances=instances, + fulfilment=ProviderFulfilment( + state=state, + message="test", + target_units=target_units if target_units is not None else n, + fulfilled_units=fulfilled_units if fulfilled_units is not None else n, + running_count=n, + pending_count=0, + failed_count=0, + ), + ) + + class TestSpotFleetHandlerCheckHostsStatus: def test_check_hosts_status_all_active(self): - """All instances active → returns all.""" + """All instances active → CheckHostsStatusResult with instances and fulfilled state.""" handler = _make_handler() - request = _make_request(["sfr-111"]) instance_ids = ["i-s1", "i-s2", "i-s3"] + request = _make_request(["sfr-111"], requested_count=len(instance_ids)) with patch.object( handler, - "_get_spot_fleet_instances", - return_value=_formatted_instances(instance_ids, "sfr-111"), + "_get_spot_fleet_status", + return_value=_fleet_status_result(instance_ids, "sfr-111"), ): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + result = handler.check_hosts_status(request) - assert len(result) == 3 - returned_ids = {r["instance_id"] for r in result} + assert isinstance(result, CheckHostsStatusResult) + assert len(result.instances) == 3 + returned_ids = {r["instance_id"] for r in result.instances} assert returned_ids == set(instance_ids) + assert isinstance(result.fulfilment, ProviderFulfilment) + assert result.fulfilment.state == "fulfilled" def test_check_hosts_status_partial_active(self): - """AWS only returns active instances via describe_spot_fleet_instances — terminated excluded.""" + """AWS only returns active instances — terminated excluded; result reflects active set.""" handler = _make_handler() - request = _make_request(["sfr-222"]) active_ids = ["i-active1", "i-active2"] + request = _make_request(["sfr-222"], requested_count=4) with patch.object( handler, - "_get_spot_fleet_instances", - return_value=_formatted_instances(active_ids, "sfr-222"), + "_get_spot_fleet_status", + return_value=_fleet_status_result( + active_ids, "sfr-222", state="in_progress", target_units=4, fulfilled_units=2 + ), ): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + result = handler.check_hosts_status(request) - assert len(result) == 2 - returned_ids = {r["instance_id"] for r in result} + assert isinstance(result, CheckHostsStatusResult) + assert len(result.instances) == 2 + returned_ids = {r["instance_id"] for r in result.instances} assert returned_ids == set(active_ids) + assert isinstance(result.fulfilment, ProviderFulfilment) def test_check_hosts_status_fleet_not_found(self): - """_get_spot_fleet_instances returns [] when fleet not found → result is [].""" + """_get_spot_fleet_status raises → error logged, skipped; result has empty instances.""" handler = _make_handler() request = _make_request(["sfr-missing"]) - with patch.object(handler, "_get_spot_fleet_instances", return_value=[]): + with patch.object( + handler, + "_get_spot_fleet_status", + side_effect=AWSInfrastructureError("Fleet not found"), + ): result = handler.check_hosts_status(request) - assert result == [] + assert isinstance(result, CheckHostsStatusResult) + assert result.instances == [] + assert isinstance(result.fulfilment, ProviderFulfilment) + assert result.fulfilment.state == "in_progress" def test_check_hosts_status_aws_error(self): - """Exception inside per-fleet loop → logged and skipped; result is [].""" + """Exception inside per-fleet loop → logged and skipped; result has empty instances.""" handler = _make_handler() request = _make_request(["sfr-err"]) with patch.object( - handler, "_get_spot_fleet_instances", side_effect=AWSInfrastructureError("AWS error") + handler, + "_get_spot_fleet_status", + side_effect=AWSInfrastructureError("AWS error"), ): result = handler.check_hosts_status(request) - assert result == [] + assert isinstance(result, CheckHostsStatusResult) + assert result.instances == [] + assert isinstance(result.fulfilment, ProviderFulfilment) def test_check_hosts_status_no_resource_ids(self): - """Empty resource_ids → returns [] without calling AWS.""" + """Empty resource_ids → returns CheckHostsStatusResult with empty instances, in_progress.""" handler = _make_handler() request = _make_request([]) - with patch.object(handler, "_get_spot_fleet_instances") as mock_get: + with patch.object(handler, "_get_spot_fleet_status") as mock_get: result = handler.check_hosts_status(request) - assert result == [] + assert isinstance(result, CheckHostsStatusResult) + assert result.instances == [] + assert isinstance(result.fulfilment, ProviderFulfilment) + assert result.fulfilment.state == "in_progress" mock_get.assert_not_called() def test_check_hosts_status_returns_correct_count(self): - """Verify count matches active instances.""" + """Verify instance count in result matches instances returned.""" handler = _make_handler() - request = _make_request(["sfr-cnt"]) instance_ids = ["i-c1", "i-c2", "i-c3", "i-c4"] + request = _make_request(["sfr-cnt"], requested_count=len(instance_ids)) with patch.object( handler, - "_get_spot_fleet_instances", - return_value=_formatted_instances(instance_ids, "sfr-cnt"), + "_get_spot_fleet_status", + return_value=_fleet_status_result(instance_ids, "sfr-cnt"), ): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + result = handler.check_hosts_status(request) - assert len(result) == 4 + assert isinstance(result, CheckHostsStatusResult) + assert len(result.instances) == 4 + assert isinstance(result.fulfilment, ProviderFulfilment) + assert result.fulfilment.state == "fulfilled" + assert result.fulfilment.target_units == 4 + assert result.fulfilment.fulfilled_units == 4 def test_check_hosts_status_preserves_instance_ids(self): - """Instance IDs in result match input.""" + """Instance IDs in result.instances match input.""" handler = _make_handler() - request = _make_request(["sfr-ids"]) instance_ids = ["i-spot-preserve1", "i-spot-preserve2"] + request = _make_request(["sfr-ids"], requested_count=len(instance_ids)) with patch.object( handler, - "_get_spot_fleet_instances", - return_value=_formatted_instances(instance_ids, "sfr-ids"), + "_get_spot_fleet_status", + return_value=_fleet_status_result(instance_ids, "sfr-ids"), ): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + result = handler.check_hosts_status(request) - returned_ids = {r["instance_id"] for r in result} + assert isinstance(result, CheckHostsStatusResult) + returned_ids = {r["instance_id"] for r in result.instances} assert returned_ids == set(instance_ids) + assert isinstance(result.fulfilment, ProviderFulfilment) def test_check_hosts_status_no_active_instances(self): - """Fleet exists but has no active instances → returns [].""" + """Fleet exists but has no active instances → empty instances, in_progress.""" handler = _make_handler() request = _make_request(["sfr-empty"]) - with patch.object(handler, "_get_spot_fleet_instances", return_value=[]): + with patch.object( + handler, + "_get_spot_fleet_status", + return_value=CheckHostsStatusResult( + instances=[], + fulfilment=ProviderFulfilment( + state="in_progress", + message="Spot Fleet waiting for instances", + target_units=3, + fulfilled_units=0, + running_count=0, + pending_count=0, + failed_count=0, + ), + ), + ): result = handler.check_hosts_status(request) - assert result == [] + assert isinstance(result, CheckHostsStatusResult) + assert result.instances == [] + assert isinstance(result.fulfilment, ProviderFulfilment) + assert result.fulfilment.state == "in_progress" def test_check_hosts_status_multiple_fleets(self): - """Multiple fleet IDs → aggregates results from all.""" + """Multiple fleet IDs → aggregates instances from all; combined fulfilment state.""" handler = _make_handler() - request = _make_request(["sfr-A", "sfr-B"]) + request = _make_request(["sfr-A", "sfr-B"], requested_count=3) ids_a = ["i-sa1", "i-sa2"] ids_b = ["i-sb1"] - def get_instances_side_effect(fleet_id, request_id): + def get_status_side_effect(fleet_id, request_id, requested_count): if fleet_id == "sfr-A": - return _formatted_instances(ids_a, "sfr-A") - return _formatted_instances(ids_b, "sfr-B") + return _fleet_status_result(ids_a, "sfr-A") + return _fleet_status_result(ids_b, "sfr-B") - with patch.object( - handler, "_get_spot_fleet_instances", side_effect=get_instances_side_effect - ): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + with patch.object(handler, "_get_spot_fleet_status", side_effect=get_status_side_effect): + result = handler.check_hosts_status(request) - assert len(result) == 3 - returned_ids = {r["instance_id"] for r in result} + assert isinstance(result, CheckHostsStatusResult) + assert len(result.instances) == 3 + returned_ids = {r["instance_id"] for r in result.instances} assert returned_ids == {"i-sa1", "i-sa2", "i-sb1"} + assert isinstance(result.fulfilment, ProviderFulfilment) + assert result.fulfilment.state == "fulfilled" def test_check_hosts_status_state_filtering_is_strict(self): - """Only instances returned by _get_spot_fleet_instances appear in result.""" + """Only instances returned by _get_spot_fleet_status appear in result.""" handler = _make_handler() - request = _make_request(["sfr-strict"]) active_ids = ["i-spot-strict-active"] + request = _make_request(["sfr-strict"], requested_count=1) with patch.object( handler, - "_get_spot_fleet_instances", - return_value=_formatted_instances(active_ids, "sfr-strict"), + "_get_spot_fleet_status", + return_value=_fleet_status_result(active_ids, "sfr-strict"), ): - with patch.object( - handler, "_format_instance_data", side_effect=lambda insts, rid, api_val: insts - ): - result = handler.check_hosts_status(request) + result = handler.check_hosts_status(request) - assert len(result) == 1 - assert result[0]["instance_id"] == "i-spot-strict-active" + assert isinstance(result, CheckHostsStatusResult) + assert len(result.instances) == 1 + assert result.instances[0]["instance_id"] == "i-spot-strict-active" + assert isinstance(result.fulfilment, ProviderFulfilment) + assert result.fulfilment.state == "fulfilled" class TestSpotFleetHandlerNameTag: diff --git a/tests/unit/providers/aws/infrastructure/test_machine_adapter.py b/tests/providers/aws/unit/infrastructure/test_machine_adapter.py similarity index 100% rename from tests/unit/providers/aws/infrastructure/test_machine_adapter.py rename to tests/providers/aws/unit/infrastructure/test_machine_adapter.py diff --git a/tests/unit/providers/aws/infrastructure/test_resource_tags.py b/tests/providers/aws/unit/infrastructure/test_resource_tags.py similarity index 100% rename from tests/unit/providers/aws/infrastructure/test_resource_tags.py rename to tests/providers/aws/unit/infrastructure/test_resource_tags.py diff --git a/tests/unit/providers/aws/utilities/ec2/__init__.py b/tests/providers/aws/unit/storage/__init__.py similarity index 100% rename from tests/unit/providers/aws/utilities/ec2/__init__.py rename to tests/providers/aws/unit/storage/__init__.py diff --git a/tests/unit/providers/aws/storage/test_aws_storage_config.py b/tests/providers/aws/unit/storage/test_aws_storage_config.py similarity index 100% rename from tests/unit/providers/aws/storage/test_aws_storage_config.py rename to tests/providers/aws/unit/storage/test_aws_storage_config.py diff --git a/tests/unit/providers/aws/storage/test_dynamodb_converter.py b/tests/providers/aws/unit/storage/test_dynamodb_converter.py similarity index 100% rename from tests/unit/providers/aws/storage/test_dynamodb_converter.py rename to tests/providers/aws/unit/storage/test_dynamodb_converter.py diff --git a/tests/unit/providers/aws/storage/test_registration_collections.py b/tests/providers/aws/unit/storage/test_registration_collections.py similarity index 100% rename from tests/unit/providers/aws/storage/test_registration_collections.py rename to tests/providers/aws/unit/storage/test_registration_collections.py diff --git a/tests/unit/providers/aws/storage/test_registration_logging.py b/tests/providers/aws/unit/storage/test_registration_logging.py similarity index 100% rename from tests/unit/providers/aws/storage/test_registration_logging.py rename to tests/providers/aws/unit/storage/test_registration_logging.py diff --git a/tests/unit/providers/aws/storage/test_registration_typed_config.py b/tests/providers/aws/unit/storage/test_registration_typed_config.py similarity index 100% rename from tests/unit/providers/aws/storage/test_registration_typed_config.py rename to tests/providers/aws/unit/storage/test_registration_typed_config.py diff --git a/tests/providers/aws/unit/strategy/__init__.py b/tests/providers/aws/unit/strategy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/providers/aws/strategy/test_augment_capacity_metadata.py b/tests/providers/aws/unit/strategy/test_augment_capacity_metadata.py similarity index 100% rename from tests/unit/providers/aws/strategy/test_augment_capacity_metadata.py rename to tests/providers/aws/unit/strategy/test_augment_capacity_metadata.py diff --git a/tests/unit/providers/aws/strategy/test_aws_provider_strategy_discovery.py b/tests/providers/aws/unit/strategy/test_aws_provider_strategy_discovery.py similarity index 100% rename from tests/unit/providers/aws/strategy/test_aws_provider_strategy_discovery.py rename to tests/providers/aws/unit/strategy/test_aws_provider_strategy_discovery.py diff --git a/tests/providers/aws/unit/strategy/test_operation_outcome.py b/tests/providers/aws/unit/strategy/test_operation_outcome.py new file mode 100644 index 000000000..2115b9197 --- /dev/null +++ b/tests/providers/aws/unit/strategy/test_operation_outcome.py @@ -0,0 +1,217 @@ +"""Unit tests for OperationOutcome discriminated union and FollowUpContext types. + +Covers: +- All four variants are instantiable and frozen (value-comparable) +- assert_never compiles and covers all branches +- FollowUpContext variants have the correct discriminator tags +- AWSProviderStrategy implements acquire/return_machines/get_status +""" + +from typing import assert_never + +import pytest + +from orb.domain.base.operation_outcome import ( + Accepted, + Completed, + Failed, + OperationOutcome, + RequiresFollowUp, +) + +# --------------------------------------------------------------------------- +# Domain types +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestOperationOutcomeVariants: + """All OperationOutcome variants must be frozen and value-comparable.""" + + def test_accepted_default_fields(self): + o = Accepted(request_id="req-1") + assert o.request_id == "req-1" + assert o.pending_resource_ids == [] + assert o.metadata == {} + + def test_accepted_with_pending_ids(self): + o = Accepted(request_id="req-1", pending_resource_ids=["i-1", "i-2"]) + assert o.pending_resource_ids == ["i-1", "i-2"] + + def test_completed_default_fields(self): + o = Completed() + assert o.resource_ids == [] + assert o.metadata == {} + + def test_completed_with_ids(self): + o = Completed(resource_ids=["i-1"]) + assert o.resource_ids == ["i-1"] + + def test_failed_default_fields(self): + o = Failed(error="something bad") + assert o.error == "something bad" + assert o.recoverable is False + assert o.metadata == {} + + def test_failed_recoverable(self): + o = Failed(error="throttle", recoverable=True) + assert o.recoverable is True + + def test_accepted_is_frozen(self): + o = Accepted(request_id="req-1") + with pytest.raises((AttributeError, TypeError)): + o.request_id = "req-2" # type: ignore[misc] + + def test_completed_is_frozen(self): + o = Completed(resource_ids=["i-1"]) + with pytest.raises((AttributeError, TypeError)): + o.resource_ids = [] # type: ignore[misc] + + def test_failed_is_frozen(self): + o = Failed(error="bad") + with pytest.raises((AttributeError, TypeError)): + o.error = "other" # type: ignore[misc] + + def test_accepted_value_equality(self): + a = Accepted(request_id="req-1", pending_resource_ids=["i-1"]) + b = Accepted(request_id="req-1", pending_resource_ids=["i-1"]) + assert a == b + + def test_completed_value_equality(self): + a = Completed(resource_ids=["i-1", "i-2"]) + b = Completed(resource_ids=["i-1", "i-2"]) + assert a == b + + +@pytest.mark.unit +class TestFollowUpContext: + """FollowUpContext variants must carry the correct discriminator tags.""" + + def test_termination_context_kind(self): + from orb.application.services.request_follow_up_context import ( + TerminationFollowUpContext, + ) + + ctx = TerminationFollowUpContext(pending_instance_ids=["i-1"]) + assert ctx.follow_up_kind == "termination" + assert ctx.expected_terminal_state == "terminated" + assert ctx.pending_instance_ids == ["i-1"] + + def test_deployment_polling_context_kind(self): + from orb.application.services.request_follow_up_context import ( + DeploymentPollingFollowUpContext, + ) + + ctx = DeploymentPollingFollowUpContext(pending_resource_ids=["fleet-1"]) + assert ctx.follow_up_kind == "deployment_polling" + assert ctx.expected_terminal_state == "running" + assert ctx.pending_resource_ids == ["fleet-1"] + + def test_requires_follow_up_wraps_context(self): + from orb.application.services.request_follow_up_context import ( + TerminationFollowUpContext, + ) + + ctx = TerminationFollowUpContext() + outcome: OperationOutcome = RequiresFollowUp(context=ctx) + assert isinstance(outcome, RequiresFollowUp) + assert outcome.context.follow_up_kind == "termination" + + def test_termination_context_is_frozen(self): + from orb.application.services.request_follow_up_context import ( + TerminationFollowUpContext, + ) + + ctx = TerminationFollowUpContext() + with pytest.raises((AttributeError, TypeError)): + ctx.expected_terminal_state = "stopped" # type: ignore[misc] + + +@pytest.mark.unit +class TestAssertNeverExhaustiveness: + """Exhaustive match over OperationOutcome must not raise and cover all branches.""" + + def _classify(self, outcome: OperationOutcome) -> str: + match outcome: + case Accepted(): + return "accepted" + case Completed(): + return "completed" + case RequiresFollowUp(): + return "requires_follow_up" + case Failed(): + return "failed" + case _ as unreachable: + assert_never(unreachable) + + def test_accepted_classified(self): + assert self._classify(Accepted(request_id="r")) == "accepted" + + def test_completed_classified(self): + assert self._classify(Completed()) == "completed" + + def test_failed_classified(self): + assert self._classify(Failed(error="e")) == "failed" + + def test_requires_follow_up_classified(self): + from orb.application.services.request_follow_up_context import TerminationFollowUpContext + + ctx = TerminationFollowUpContext() + assert self._classify(RequiresFollowUp(context=ctx)) == "requires_follow_up" + + +# --------------------------------------------------------------------------- +# AWSProviderStrategy — interface contract +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestAWSProviderStrategyOutcomeInterface: + """AWSProviderStrategy must declare acquire / return_machines / get_status.""" + + def test_acquire_method_exists(self): + + from orb.providers.aws.strategy.aws_provider_strategy import AWSProviderStrategy + + assert hasattr(AWSProviderStrategy, "acquire") + assert asyncio_or_coroutine(AWSProviderStrategy.acquire) + + def test_return_machines_method_exists(self): + from orb.providers.aws.strategy.aws_provider_strategy import AWSProviderStrategy + + assert hasattr(AWSProviderStrategy, "return_machines") + assert asyncio_or_coroutine(AWSProviderStrategy.return_machines) + + def test_get_status_method_exists(self): + from orb.providers.aws.strategy.aws_provider_strategy import AWSProviderStrategy + + assert hasattr(AWSProviderStrategy, "get_status") + assert asyncio_or_coroutine(AWSProviderStrategy.get_status) + + def test_base_strategy_declares_acquire_abstract(self): + + from orb.providers.base.strategy.base_provider_strategy import BaseProviderStrategy + + assert "acquire" in {m for m in dir(BaseProviderStrategy)} + method = getattr(BaseProviderStrategy, "acquire") + assert getattr(method, "__isabstractmethod__", False) + + def test_base_strategy_declares_return_machines_abstract(self): + from orb.providers.base.strategy.base_provider_strategy import BaseProviderStrategy + + method = getattr(BaseProviderStrategy, "return_machines") + assert getattr(method, "__isabstractmethod__", False) + + def test_base_strategy_declares_get_status_abstract(self): + from orb.providers.base.strategy.base_provider_strategy import BaseProviderStrategy + + method = getattr(BaseProviderStrategy, "get_status") + assert getattr(method, "__isabstractmethod__", False) + + +def asyncio_or_coroutine(fn) -> bool: + """Return True if fn is an async function.""" + import asyncio + import inspect + + return asyncio.iscoroutinefunction(fn) or inspect.iscoroutinefunction(fn) diff --git a/tests/unit/providers/aws/test_allocation_strategy.py b/tests/providers/aws/unit/test_allocation_strategy.py similarity index 99% rename from tests/unit/providers/aws/test_allocation_strategy.py rename to tests/providers/aws/unit/test_allocation_strategy.py index 167b4d357..4f95dc27e 100644 --- a/tests/unit/providers/aws/test_allocation_strategy.py +++ b/tests/providers/aws/unit/test_allocation_strategy.py @@ -2,7 +2,7 @@ import pytest -from orb.providers.aws.domain.template.value_objects import ( +from orb.providers.aws.value_objects import ( AWSAllocationStrategy, normalise_allocation_strategy, ) diff --git a/tests/providers/aws/unit/test_aws_error_propagation.py b/tests/providers/aws/unit/test_aws_error_propagation.py new file mode 100644 index 000000000..2cc063867 --- /dev/null +++ b/tests/providers/aws/unit/test_aws_error_propagation.py @@ -0,0 +1,382 @@ +"""Unit tests: AWS ClientError details are captured and propagated to the request. + +Covers: + - _convert_client_error preserves aws_error_code, aws_error_message, + aws_request_id, error_source on every mapped exception subclass. + - Access-key redaction in error messages. + - _extract_aws_error_fields helper extracts attrs from AWSError subclasses + and returns None values for non-AWSError exceptions. + - ProvisioningResult carries AWS error fields through from the exception. + - RequestStatusManagementService._handle_provisioning_failure writes + error_details["provider_error"] onto the Request aggregate. + - RequestDTO.from_domain exposes error_details["provider_error"] (or legacy + "aws_error") as the top-level error field; to_dict includes it only when present. +""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +from botocore.exceptions import ClientError + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_client_error( + code: str, + message: str, + request_id: str = "req-abc-123", + http_status: int = 403, +) -> ClientError: + """Build a botocore ClientError with realistic response metadata.""" + return ClientError( + { + "Error": {"Code": code, "Message": message}, + "ResponseMetadata": { + "RequestId": request_id, + "HTTPStatusCode": http_status, + }, + }, + "RunInstances", + ) + + +def _make_handler(): + """Minimal AWSHandler-like object exposing _convert_client_error.""" + from orb.providers.aws.infrastructure.handlers.base_handler import AWSHandler + + # AWSHandler is abstract; build via a minimal concrete subclass + class _Handler(AWSHandler): + def _acquire_hosts_internal(self, request, aws_template): + pass # type: ignore[return] + + def check_hosts_status(self, request): + pass # type: ignore[return] + + def release_hosts(self, machine_ids, resource_mapping=None, request_id=""): + pass + + def cancel_resource(self, resource_id, request_id): + pass # type: ignore[return] + + @classmethod + def get_example_templates(cls): + return [] + + return _Handler( + aws_client=MagicMock(), + logger=MagicMock(), + aws_ops=MagicMock(), + launch_template_manager=MagicMock(), + ) + + +# --------------------------------------------------------------------------- +# _convert_client_error — error field propagation +# --------------------------------------------------------------------------- + + +class TestConvertClientError: + """_convert_client_error transfers AWS error metadata to the domain exception.""" + + def setup_method(self): + self.handler = _make_handler() + + def _check_aws_fields(self, exc, expected_code, expected_message, expected_request_id): + assert getattr(exc, "aws_error_code", None) == expected_code + assert getattr(exc, "aws_error_message", None) == expected_message + assert getattr(exc, "aws_request_id", None) == expected_request_id + assert getattr(exc, "error_source", None) is not None + + def test_unauthorized_operation_carries_aws_fields(self): + err = _make_client_error("UnauthorizedOperation", "You are not authorized.", "rid-001") + exc = self.handler._convert_client_error(err, "run_instances") + self._check_aws_fields(exc, "UnauthorizedOperation", "You are not authorized.", "rid-001") + + def test_access_denied_carries_aws_fields(self): + err = _make_client_error("AccessDenied", "Access denied.", "rid-002") + exc = self.handler._convert_client_error(err, "create_fleet") + self._check_aws_fields(exc, "AccessDenied", "Access denied.", "rid-002") + + def test_insufficient_instance_capacity_carries_aws_fields(self): + err = _make_client_error( + "InsufficientInstanceCapacity", "Insufficient capacity.", "rid-003" + ) + exc = self.handler._convert_client_error(err, "run_instances") + # InsufficientInstanceCapacity falls to the AWSInfrastructureError else-branch + # which also carries AWS error fields. + self._check_aws_fields( + exc, "InsufficientInstanceCapacity", "Insufficient capacity.", "rid-003" + ) + + def test_quota_exceeded_carries_aws_fields(self): + err = _make_client_error("InstanceLimitExceeded", "Limit exceeded.", "rid-004") + exc = self.handler._convert_client_error(err, "run_instances") + self._check_aws_fields(exc, "InstanceLimitExceeded", "Limit exceeded.", "rid-004") + + def test_error_source_contains_operation_name(self): + err = _make_client_error("UnauthorizedOperation", "Denied.", "rid-005") + exc = self.handler._convert_client_error(err, "run_instances") + assert "run_instances" in (getattr(exc, "error_source", "") or "") + + def test_missing_response_metadata_does_not_raise(self): + """Gracefully handle ClientError without ResponseMetadata.RequestId.""" + client_err = ClientError( + {"Error": {"Code": "UnauthorizedOperation", "Message": "Denied."}}, + "RunInstances", + ) + exc = self.handler._convert_client_error(client_err, "run_instances") + # aws_request_id should be None (key absent) but must not raise + assert getattr(exc, "aws_request_id", "sentinel") is None + + def test_access_key_redacted_in_message(self): + """AWS error messages containing access-key tokens are redacted.""" + message = "User AKIAIOSFODNN7EXAMPLE is not authorized to perform this operation." + err = _make_client_error("UnauthorizedOperation", message, "rid-006") + exc = self.handler._convert_client_error(err, "run_instances") + assert "AKIAIOSFODNN7EXAMPLE" not in str(exc) + assert getattr(exc, "aws_error_message", "") is not None + assert "AKIAIOSFODNN7EXAMPLE" not in (getattr(exc, "aws_error_message", "") or "") + + def test_sts_access_key_prefix_redacted(self): + """Temporary credential key IDs (ASIA prefix) are also redacted.""" + message = "ASIAIOSFODNN7EXAMPLE is invalid." + err = _make_client_error("AccessDenied", message, "rid-007") + exc = self.handler._convert_client_error(err, "run_instances") + assert "ASIAIOSFODNN7EXAMPLE" not in (getattr(exc, "aws_error_message", "") or "") + + +# --------------------------------------------------------------------------- +# _extract_aws_error_fields helper +# --------------------------------------------------------------------------- + + +class TestExtractAwsErrorFields: + """_extract_aws_error_fields returns correct dict for AWSError and falls back for others.""" + + def test_returns_aws_fields_from_aws_error(self): + from orb.application.services.provisioning_orchestration_service import ( + _extract_aws_error_fields, + ) + from orb.providers.aws.exceptions.aws_exceptions import AuthorizationError + + exc = AuthorizationError( + "denied", + aws_error_code="UnauthorizedOperation", + aws_error_message="You are not authorized.", + aws_request_id="rid-xyz", + error_source="aws.ec2.run_instances", + ) + result = _extract_aws_error_fields(exc) + assert result["provider_error_code"] == "UnauthorizedOperation" + assert result["provider_error_message"] == "You are not authorized." + assert result["provider_request_id"] == "rid-xyz" + assert result["error_source"] == "aws.ec2.run_instances" + + def test_returns_none_values_for_generic_exception(self): + from orb.application.services.provisioning_orchestration_service import ( + _extract_aws_error_fields, + ) + + result = _extract_aws_error_fields(ValueError("some error")) + assert result["provider_error_code"] is None + assert result["provider_error_message"] is None + assert result["provider_request_id"] is None + assert result["error_source"] is None + + def test_partial_aws_error_attrs(self): + from orb.application.services.provisioning_orchestration_service import ( + _extract_aws_error_fields, + ) + from orb.providers.aws.exceptions.aws_exceptions import AWSInfrastructureError + + exc = AWSInfrastructureError( + "infra error", + aws_error_code="InsufficientInstanceCapacity", + ) + result = _extract_aws_error_fields(exc) + assert result["provider_error_code"] == "InsufficientInstanceCapacity" + assert result["provider_error_message"] is None + + +# --------------------------------------------------------------------------- +# RequestStatusManagementService._handle_provisioning_failure +# --------------------------------------------------------------------------- + + +class TestHandleProvisioningFailure: + """error_details["provider_error"] is written when provisioning fails with AWS context.""" + + def _make_service(self): + from orb.application.services.request_status_management_service import ( + RequestStatusManagementService, + ) + + return RequestStatusManagementService(uow_factory=MagicMock(), logger=MagicMock()) + + def _make_provisioning_result( + self, + *, + success: bool = False, + resource_ids: list[str] | None = None, + machine_ids: list[str] | None = None, + instances: list[dict] | None = None, + provider_data: dict | None = None, + error_message: str | None = "Provisioning failed", + aws_error_code: str | None = None, + aws_error_message: str | None = None, + aws_request_id: str | None = None, + error_source: str | None = None, + ): + from orb.application.services.provisioning_orchestration_service import ProvisioningResult + + return ProvisioningResult( + success=success, + resource_ids=resource_ids if resource_ids is not None else [], + machine_ids=machine_ids if machine_ids is not None else [], + instances=instances if instances is not None else [], + provider_data=provider_data if provider_data is not None else {}, + error_message=error_message, + provider_error_code=aws_error_code, + provider_error_message=aws_error_message, + provider_request_id=aws_request_id, + error_source=error_source, + ) + + def _make_real_request(self): + """Build a real Request aggregate (not a mock) so model_copy works.""" + from orb.domain.request.aggregate import Request + from orb.domain.request.value_objects import RequestType + + # Let Request generate its own UUID-based ID + return Request.create_new_request( + request_type=RequestType.ACQUIRE, + template_id="tpl-test", + machine_count=1, + provider_type="aws", + ) + + def test_no_aws_fields_leaves_error_details_clean(self): + """When no AWS-specific fields present, error_details stays clean.""" + svc = self._make_service() + request = self._make_real_request() + result = self._make_provisioning_result(error_message="generic error") + updated = svc._handle_provisioning_failure(request, result) + # aws_error key should be absent when no AWS details are available + assert "provider_error" not in updated.error_details + + def test_aws_error_code_written_to_error_details(self): + """UnauthorizedOperation is stored in error_details["provider_error"]["code"].""" + svc = self._make_service() + request = self._make_real_request() + result = self._make_provisioning_result( + error_message="AWS error", + aws_error_code="UnauthorizedOperation", + aws_error_message="You are not authorized.", + aws_request_id="rid-aws-001", + error_source="aws.ec2.run_instances", + ) + updated = svc._handle_provisioning_failure(request, result) + aws_err = updated.error_details.get("provider_error", {}) + assert aws_err["code"] == "UnauthorizedOperation" + assert aws_err["message"] == "You are not authorized." + assert aws_err["aws_request_id"] == "rid-aws-001" + assert aws_err["source"] == "aws.ec2.run_instances" + + def test_partial_aws_fields_stored(self): + """Only non-None AWS fields are stored — missing ones are absent from the block.""" + svc = self._make_service() + request = self._make_real_request() + result = self._make_provisioning_result( + error_message="AWS error", + aws_error_code="InsufficientInstanceCapacity", + # aws_error_message, aws_request_id, error_source intentionally absent + ) + updated = svc._handle_provisioning_failure(request, result) + aws_err = updated.error_details.get("provider_error", {}) + assert aws_err["code"] == "InsufficientInstanceCapacity" + assert "message" not in aws_err + assert "aws_request_id" not in aws_err + + +# --------------------------------------------------------------------------- +# RequestDTO.from_domain — error field population +# --------------------------------------------------------------------------- + + +class TestRequestDTOErrorField: + """RequestDTO.from_domain exposes aws_error as the top-level error field.""" + + def _make_request_with_error(self, aws_error_block: dict): + + from orb.domain.request.aggregate import Request + from orb.domain.request.value_objects import RequestType + + request = Request.create_new_request( + request_type=RequestType.ACQUIRE, + template_id="tpl-1", + machine_count=1, + provider_type="aws", + ) + # inject error_details + from orb.domain.request.request_types import RequestStatus + + updated_fields = request.model_dump() + updated_fields["error_details"] = {"aws_error": aws_error_block} + updated_fields["status"] = RequestStatus.FAILED + return Request.model_validate(updated_fields) + + def test_error_field_present_when_aws_error_in_details(self): + from orb.application.request.dto import RequestDTO + + request = self._make_request_with_error( + {"code": "UnauthorizedOperation", "message": "not authorized"} + ) + dto = RequestDTO.from_domain(request) + assert dto.error is not None + assert dto.error["code"] == "UnauthorizedOperation" + assert dto.error["message"] == "not authorized" + + def test_error_field_none_when_no_aws_error(self): + + from orb.application.request.dto import RequestDTO + from orb.domain.request.aggregate import Request + from orb.domain.request.value_objects import RequestType + + request = Request.create_new_request( + request_type=RequestType.ACQUIRE, + template_id="tpl-1", + machine_count=1, + provider_type="aws", + ) + dto = RequestDTO.from_domain(request) + assert dto.error is None + + def test_to_dict_includes_error_block_when_present(self): + from orb.application.request.dto import RequestDTO + + request = self._make_request_with_error( + {"code": "AccessDenied", "source": "aws.ec2.run_instances"} + ) + dto = RequestDTO.from_domain(request) + d = dto.to_dict() + assert "error" in d + assert d["error"]["code"] == "AccessDenied" + + def test_to_dict_omits_error_key_when_no_error(self): + + from orb.application.request.dto import RequestDTO + from orb.domain.request.aggregate import Request + from orb.domain.request.value_objects import RequestType + + request = Request.create_new_request( + request_type=RequestType.ACQUIRE, + template_id="tpl-1", + machine_count=1, + provider_type="aws", + ) + dto = RequestDTO.from_domain(request) + d = dto.to_dict() + assert "error" not in d diff --git a/tests/unit/providers/test_aws_handlers.py b/tests/providers/aws/unit/test_aws_handlers.py similarity index 100% rename from tests/unit/providers/test_aws_handlers.py rename to tests/providers/aws/unit/test_aws_handlers.py diff --git a/tests/unit/providers/aws/test_aws_native_spec_service.py b/tests/providers/aws/unit/test_aws_native_spec_service.py similarity index 100% rename from tests/unit/providers/aws/test_aws_native_spec_service.py rename to tests/providers/aws/unit/test_aws_native_spec_service.py diff --git a/tests/unit/providers/aws/test_aws_native_spec_service_merge.py b/tests/providers/aws/unit/test_aws_native_spec_service_merge.py similarity index 100% rename from tests/unit/providers/aws/test_aws_native_spec_service_merge.py rename to tests/providers/aws/unit/test_aws_native_spec_service_merge.py diff --git a/tests/unit/providers/aws/test_aws_provider_config_legacy_timeout.py b/tests/providers/aws/unit/test_aws_provider_config_legacy_timeout.py similarity index 100% rename from tests/unit/providers/aws/test_aws_provider_config_legacy_timeout.py rename to tests/providers/aws/unit/test_aws_provider_config_legacy_timeout.py diff --git a/tests/unit/providers/aws/test_base_context_mixin.py b/tests/providers/aws/unit/test_base_context_mixin.py similarity index 100% rename from tests/unit/providers/aws/test_base_context_mixin.py rename to tests/providers/aws/unit/test_base_context_mixin.py diff --git a/tests/providers/aws/unit/test_cognito_strategy.py b/tests/providers/aws/unit/test_cognito_strategy.py new file mode 100644 index 000000000..3c3c097a5 --- /dev/null +++ b/tests/providers/aws/unit/test_cognito_strategy.py @@ -0,0 +1,453 @@ +"""Unit tests for CognitoAuthStrategy.""" + +from __future__ import annotations + +import time +from base64 import urlsafe_b64encode +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from orb.infrastructure.adapters.ports.auth import AuthContext, AuthStatus + + +def _make_context(**kwargs) -> AuthContext: + defaults: dict[str, Any] = { + "method": "GET", + "path": "/api/v1/machines", + "headers": {}, + "query_params": {}, + } + defaults.update(kwargs) + return AuthContext(**defaults) + + +def _make_logger() -> MagicMock: + logger = MagicMock() + logger.debug = MagicMock() + logger.info = MagicMock() + logger.warning = MagicMock() + logger.error = MagicMock() + return logger + + +def _make_strategy(enabled: bool = True) -> Any: + from orb.providers.aws.auth.cognito_strategy import CognitoAuthStrategy + + logger = _make_logger() + + # Clear the class-level JWKS cache so each test starts from a clean state. + CognitoAuthStrategy._jwks_cache.clear() + + with patch("boto3.client") as _mock_boto3: + _mock_boto3.return_value = MagicMock() + strategy = CognitoAuthStrategy( + logger=logger, + user_pool_id="us-east-1_EXAMPLE", + client_id="abc123", + region="us-east-1", + enabled=enabled, + ) + + return strategy + + +# --------------------------------------------------------------------------- +# authenticate() — missing Authorization header +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_cognito_authenticate_missing_header(): + """Cognito authenticate returns FAILED when Authorization header is missing.""" + strategy = _make_strategy() + result = await strategy.authenticate(_make_context(headers={})) + + assert result.status == AuthStatus.FAILED + assert "authorization" in (result.error_message or "").lower() + + +# --------------------------------------------------------------------------- +# authenticate() — disabled strategy +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_cognito_authenticate_disabled(): + """Cognito authenticate returns FAILED when strategy is disabled.""" + strategy = _make_strategy(enabled=False) + + result = await strategy.authenticate( + _make_context(headers={"authorization": "Bearer sometoken"}) + ) + + assert result.status == AuthStatus.FAILED + assert "disabled" in (result.error_message or "").lower() + + +# --------------------------------------------------------------------------- +# validate_token() — expired token +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_cognito_validate_token_expired(): + """validate_token returns EXPIRED for an expired JWT.""" + import jwt as pyjwt + + strategy = _make_strategy() + + with patch.object(strategy, "_get_public_key", return_value="fake_key"): + with patch("orb.providers.aws.auth.cognito_strategy.jwt.decode") as mock_decode: + mock_decode.side_effect = pyjwt.ExpiredSignatureError("expired") + # Need an unverified header + with patch( + "orb.providers.aws.auth.cognito_strategy.jwt.get_unverified_header", + return_value={"kid": "test-kid"}, + ): + result = await strategy.validate_token("expired.token.here") + + assert result.status == AuthStatus.EXPIRED + + +# --------------------------------------------------------------------------- +# validate_token() — no key ID in header +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_cognito_validate_token_missing_kid(): + """validate_token returns INVALID when token header has no kid.""" + + strategy = _make_strategy() + + with patch( + "orb.providers.aws.auth.cognito_strategy.jwt.get_unverified_header", + return_value={}, # no "kid" + ): + result = await strategy.validate_token("some.token.here") + + assert result.status == AuthStatus.INVALID + + +# --------------------------------------------------------------------------- +# _map_groups_to_roles +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +def test_cognito_map_groups_to_roles_admin(): + """Admin group maps to admin role.""" + strategy = _make_strategy() + roles = strategy._map_groups_to_roles(["admin"]) + assert "admin" in roles + + +@pytest.mark.unit +def test_cognito_map_groups_to_roles_unknown_group(): + """Unknown groups do not add extra roles beyond the default user role.""" + strategy = _make_strategy() + roles = strategy._map_groups_to_roles(["unknown-group"]) + assert roles == ["user"] + + +# --------------------------------------------------------------------------- +# from_auth_config classmethod +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +def test_cognito_from_auth_config_defaults(): + """from_auth_config builds strategy with defaults when no sub-config given.""" + from orb.config.schemas.server_schema import AuthConfig + + auth_config = AuthConfig(strategy="cognito") # type: ignore[call-arg] # pydantic default fields + + with patch("boto3.client") as _mock: + _mock.return_value = MagicMock() + from orb.providers.aws.auth.cognito_strategy import CognitoAuthStrategy + + strategy = CognitoAuthStrategy.from_auth_config(auth_config) + + assert strategy.region == "us-east-1" + assert strategy.user_pool_id == "" + assert strategy.client_id == "" + + +# --------------------------------------------------------------------------- +# _get_public_key — non-RSA key raises InvalidTokenError +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_cognito_get_public_key_non_rsa_raises(): + """_get_public_key raises InvalidTokenError when the matched JWK is not RSA.""" + import jwt as pyjwt + + strategy = _make_strategy() + + ec_jwks = { + "keys": [ + { + "kid": "test-kid", + "kty": "EC", + "crv": "P-256", + "x": "abc", + "y": "def", + } + ] + } + + mock_response = MagicMock() + mock_response.json.return_value = ec_jwks + + with patch("requests.get", return_value=mock_response): + with pytest.raises(pyjwt.InvalidTokenError): + await strategy._get_public_key("test-kid") + + +# --------------------------------------------------------------------------- +# validate_token — full round-trip with a real RS256 token +# --------------------------------------------------------------------------- + + +def _generate_rs256_jwks_and_token( + user_pool_id: str, + client_id: str, + region: str, + kid: str = "test-key-1", +) -> tuple[dict[str, Any], str]: + """ + Generate a minimal JWKS dict and a matching RS256 JWT for testing. + + Returns (jwks_dict, encoded_token). + """ + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers + + # Generate a 2048-bit RSA key pair + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + public_key = private_key.public_key() + pub_numbers: RSAPublicNumbers = public_key.public_numbers() # type: ignore[attr-defined] + + def _int_to_b64url(n: int) -> str: + byte_length = (n.bit_length() + 7) // 8 + raw = n.to_bytes(byte_length, "big") + return urlsafe_b64encode(raw).rstrip(b"=").decode() + + jwks: dict[str, Any] = { + "keys": [ + { + "kid": kid, + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "n": _int_to_b64url(pub_numbers.n), + "e": _int_to_b64url(pub_numbers.e), + } + ] + } + + now = int(time.time()) + payload = { + "sub": "user-cognito-123", + "cognito:username": "testuser", + "email": "test@example.com", + "cognito:groups": ["operators"], + "token_use": "access", + "aud": client_id, + "iss": f"https://cognito-idp.{region}.amazonaws.com/{user_pool_id}", + "iat": now, + "exp": now + 3600, + } + + import jwt as pyjwt + + token = pyjwt.encode( + payload, + private_key, + algorithm="RS256", + headers={"kid": kid}, + ) + return jwks, token + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_cognito_validate_token_full_rs256_round_trip(): + """ + Full validate_token round-trip: mocked JWKS + real RS256 token must succeed. + + This test would fail if _get_public_key returned a raw JWK dict (the pre-fix + behaviour) because PyJWT cannot decode with a dict key. + """ + user_pool_id = "us-east-1_TESTPOOL" + client_id = "test-client-id" + region = "us-east-1" + + jwks, token = _generate_rs256_jwks_and_token(user_pool_id, client_id, region) + + mock_response = MagicMock() + mock_response.json.return_value = jwks + + strategy = _make_strategy() + strategy.user_pool_id = user_pool_id + strategy.client_id = client_id + strategy.region = region + + with patch("requests.get", return_value=mock_response): + result = await strategy.validate_token(token) + + assert result.status == AuthStatus.SUCCESS + assert result.user_id == "user-cognito-123" + assert "operator" in (result.user_roles or []) + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_cognito_validate_token_unknown_kid_returns_invalid(): + """validate_token returns INVALID when no JWKS entry matches the token's kid.""" + user_pool_id = "us-east-1_TESTPOOL" + client_id = "test-client-id" + region = "us-east-1" + + # Generate a valid JWKS+token but serve JWKS with a *different* kid + jwks, token = _generate_rs256_jwks_and_token(user_pool_id, client_id, region, kid="actual-kid") + # Replace kid in JWKS so the lookup fails + jwks["keys"][0]["kid"] = "different-kid" + + mock_response = MagicMock() + mock_response.json.return_value = jwks + + strategy = _make_strategy() + strategy.user_pool_id = user_pool_id + strategy.client_id = client_id + strategy.region = region + + with patch("requests.get", return_value=mock_response): + result = await strategy.validate_token(token) + + assert result.status == AuthStatus.INVALID + + +# --------------------------------------------------------------------------- +# _get_public_key — raise_for_status called on JWKS response +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_get_public_key_calls_raise_for_status(): + """_get_public_key must call raise_for_status() on the JWKS HTTP response. + + A Cognito 403 or 503 returns a non-keys JSON body; without raise_for_status() + the kid-lookup silently returns None and every token is rejected with an + opaque "Unable to verify token signature" error that is hard to diagnose. + """ + strategy = _make_strategy() + + mock_response = MagicMock() + mock_response.json.return_value = {"keys": []} + + with patch("requests.get", return_value=mock_response) as mock_get: + await strategy._get_public_key("any-kid") + + mock_response.raise_for_status.assert_called_once() + mock_get.assert_called_once_with(strategy.jwks_url, timeout=30) + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_get_public_key_non_2xx_propagates_as_invalid_token(): + """An HTTP error from the JWKS endpoint propagates and causes INVALID token result. + + The HTTPError raised by raise_for_status() is caught by the outer except clause + in _get_public_key, logged, and returns None — which validate_token converts + to an INVALID AuthResult rather than swallowing the error silently. + """ + import requests as req + + strategy = _make_strategy() + + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = req.exceptions.HTTPError("403 Forbidden") + + with patch( + "orb.providers.aws.auth.cognito_strategy.jwt.get_unverified_header", + return_value={"kid": "test-kid"}, + ): + with patch("requests.get", return_value=mock_response): + result = await strategy.validate_token("some.bearer.token") + + assert result.status == AuthStatus.INVALID + assert "Unable to verify token signature" in (result.error_message or "") + + +# --------------------------------------------------------------------------- +# JWKS caching behaviour +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_jwks_fetch_is_cached(): + """_get_public_key called twice with the same kid only fetches JWKS once.""" + strategy = _make_strategy() + + mock_response = MagicMock() + mock_response.json.return_value = {"keys": []} # kid not found → returns None both times + + with patch("requests.get", return_value=mock_response) as mock_get: + await strategy._get_public_key("some-kid") + await strategy._get_public_key("some-kid") + + # The HTTP fetch should happen only once; second call should hit the cache. + mock_get.assert_called_once() + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_jwks_cache_evicted_on_invalid_key_type(): + """Cache entry is evicted when _get_public_key raises InvalidTokenError for a non-RSA key. + + A subsequent call should re-fetch from the endpoint so that a key rotation + is picked up without requiring a process restart. + """ + import jwt as pyjwt + + strategy = _make_strategy() + + ec_jwks = { + "keys": [ + { + "kid": "ec-kid", + "kty": "EC", + "crv": "P-256", + "x": "abc", + "y": "def", + } + ] + } + + mock_response = MagicMock() + mock_response.json.return_value = ec_jwks + + with patch("requests.get", return_value=mock_response) as mock_get: + # First call: fetches JWKS and raises because key type is not RSA. + with pytest.raises(pyjwt.InvalidTokenError): + await strategy._get_public_key("ec-kid") + + # Cache entry must have been evicted on the error. + assert strategy.jwks_url not in strategy._jwks_cache + + # Second call: must re-fetch (not serve from cache). + with pytest.raises(pyjwt.InvalidTokenError): + await strategy._get_public_key("ec-kid") + + assert mock_get.call_count == 2 diff --git a/tests/unit/providers/aws/test_handler_merge_integration.py b/tests/providers/aws/unit/test_handler_merge_integration.py similarity index 100% rename from tests/unit/providers/aws/test_handler_merge_integration.py rename to tests/providers/aws/unit/test_handler_merge_integration.py diff --git a/tests/providers/aws/unit/test_iam_strategy.py b/tests/providers/aws/unit/test_iam_strategy.py new file mode 100644 index 000000000..c7cc00733 --- /dev/null +++ b/tests/providers/aws/unit/test_iam_strategy.py @@ -0,0 +1,285 @@ +"""Unit tests for IAMAuthStrategy.""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from orb.infrastructure.adapters.ports.auth import AuthContext, AuthStatus + + +def _make_context(**kwargs) -> AuthContext: + defaults: dict[str, Any] = { + "method": "GET", + "path": "/api/v1/machines", + "headers": {}, + "query_params": {}, + } + defaults.update(kwargs) + return AuthContext(**defaults) + + +def _make_logger() -> MagicMock: + logger = MagicMock() + logger.debug = MagicMock() + logger.info = MagicMock() + logger.warning = MagicMock() + logger.error = MagicMock() + return logger + + +def _make_strategy(assume_permissions: bool = True) -> Any: + """Build IAMAuthStrategy with mocked AWS clients.""" + from orb.providers.aws.auth.iam_strategy import IAMAuthStrategy + + logger = _make_logger() + + with patch("orb.providers.aws.auth.iam_strategy.AWSSessionFactory") as mock_factory: + mock_session = MagicMock() + mock_sts = MagicMock() + mock_iam = MagicMock() + mock_session.client.side_effect = lambda svc, **kw: mock_sts if svc == "sts" else mock_iam + mock_factory.create_session.return_value = mock_session + + strategy = IAMAuthStrategy( + logger=logger, + region="us-east-1", + assume_permissions=assume_permissions, + ) + + strategy.sts_client = mock_sts + strategy.iam_client = mock_iam + return strategy + + +# --------------------------------------------------------------------------- +# authenticate() — happy path +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_iam_authenticate_success_assume_permissions(monkeypatch): + """IAM authenticate returns SUCCESS with permissions when assume_permissions=True and dev env var is set.""" + monkeypatch.setenv("ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY", "true") + + strategy = _make_strategy(assume_permissions=True) + strategy.sts_client.get_caller_identity.return_value = { + "UserId": "AIDAEXAMPLE", + "Account": "123456789012", + "Arn": "arn:aws:iam::123456789012:user/testuser", + } + + result = await strategy.authenticate(_make_context()) + + assert result.status == AuthStatus.SUCCESS + assert result.user_id is not None + assert len(result.permissions) > 0 + + +# --------------------------------------------------------------------------- +# authenticate() — disabled strategy +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_iam_authenticate_disabled(): + """IAM authenticate returns FAILED when strategy is disabled.""" + strategy = _make_strategy() + strategy.enabled = False + + result = await strategy.authenticate(_make_context()) + + assert result.status == AuthStatus.FAILED + assert "disabled" in (result.error_message or "").lower() + + +# --------------------------------------------------------------------------- +# authenticate() — no credentials +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_iam_authenticate_no_credentials(): + """IAM authenticate returns FAILED when AWS credentials are absent.""" + from botocore.exceptions import NoCredentialsError + + strategy = _make_strategy() + strategy.sts_client.get_caller_identity.side_effect = NoCredentialsError() + + result = await strategy.authenticate(_make_context()) + + assert result.status == AuthStatus.FAILED + assert "credentials" in (result.error_message or "").lower() + + +# --------------------------------------------------------------------------- +# _check_permissions — assume_permissions=False → deny +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_iam_check_permissions_deny_without_assume(): + """_check_permissions returns empty list when assume_permissions is False.""" + strategy = _make_strategy(assume_permissions=False) + strategy._assume_permissions = False + + permissions = await strategy._check_permissions({"Arn": "arn:aws:iam::123:user/u"}) + + assert permissions == [] + + +# --------------------------------------------------------------------------- +# _determine_roles — admin detection +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_iam_determine_roles_admin(): + """_determine_roles detects admin when ARN contains a known admin pattern.""" + strategy = _make_strategy() + + roles = await strategy._determine_roles( + {"Arn": "arn:aws:iam::123456789012:user/Admin"}, + ["ec2:DescribeInstances"], + ) + + assert "admin" in roles + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_iam_determine_roles_service_account(): + """_determine_roles adds service_account role for role-based ARNs.""" + strategy = _make_strategy() + + roles = await strategy._determine_roles( + {"Arn": "arn:aws:iam::123:role/my-service-role"}, + [], + ) + + assert "service_account" in roles + + +# --------------------------------------------------------------------------- +# from_auth_config classmethod +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +def test_iam_from_auth_config_defaults(): + """from_auth_config builds strategy using IAMAuthSubConfig defaults.""" + from orb.config.schemas.server_schema import AuthConfig + + auth_config = AuthConfig(strategy="iam") # type: ignore[call-arg] # pydantic default fields + + with patch("orb.providers.aws.auth.iam_strategy.AWSSessionFactory") as mock_factory: + mock_session = MagicMock() + mock_session.client.return_value = MagicMock() + mock_factory.create_session.return_value = mock_session + + from orb.providers.aws.auth.iam_strategy import IAMAuthStrategy + + strategy = IAMAuthStrategy.from_auth_config(auth_config) + + assert strategy.region == "us-east-1" + assert strategy._assume_permissions is False + + +# --------------------------------------------------------------------------- +# assume_permissions=True without env var → silently disabled + CRITICAL logged +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +def test_iam_assume_permissions_without_env_var_is_disabled(monkeypatch): + """ + assume_permissions=True in config is ignored when + ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY is not set. + A CRITICAL warning must be logged and _assume_permissions must be False. + """ + monkeypatch.delenv("ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY", raising=False) + + from orb.providers.aws.auth.iam_strategy import IAMAuthStrategy + + logger = _make_logger() + + with patch("orb.providers.aws.auth.iam_strategy.AWSSessionFactory") as mock_factory: + mock_session = MagicMock() + mock_session.client.return_value = MagicMock() + mock_factory.create_session.return_value = mock_session + + strategy = IAMAuthStrategy( + logger=logger, + region="us-east-1", + assume_permissions=True, # config says True … + ) + + # … but env var absent → must be disabled + assert strategy._assume_permissions is False + + # CRITICAL must have been logged to warn the operator + critical_calls = logger.critical.call_args_list + assert critical_calls, "Expected at least one logger.critical() call" + combined = " ".join(str(c) for c in critical_calls) + assert "ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY" in combined + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_iam_assume_permissions_without_env_var_returns_empty_permissions(monkeypatch): + """ + When assume_permissions=True but env var is absent, _check_permissions + returns an empty list (deny-all). + """ + monkeypatch.delenv("ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY", raising=False) + + from orb.providers.aws.auth.iam_strategy import IAMAuthStrategy + + logger = _make_logger() + + with patch("orb.providers.aws.auth.iam_strategy.AWSSessionFactory") as mock_factory: + mock_session = MagicMock() + mock_session.client.return_value = MagicMock() + mock_factory.create_session.return_value = mock_session + + strategy = IAMAuthStrategy( + logger=logger, + region="us-east-1", + assume_permissions=True, + ) + + permissions = await strategy._check_permissions({"Arn": "arn:aws:iam::123:user/u"}) + assert permissions == [] + + +@pytest.mark.unit +def test_iam_assume_permissions_with_env_var_is_active(monkeypatch): + """ + assume_permissions=True is honoured when + ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY=true is set. + """ + monkeypatch.setenv("ORB_IAM_ASSUME_PERMISSIONS_DEV_ONLY", "true") + + from orb.providers.aws.auth.iam_strategy import IAMAuthStrategy + + logger = _make_logger() + + with patch("orb.providers.aws.auth.iam_strategy.AWSSessionFactory") as mock_factory: + mock_session = MagicMock() + mock_session.client.return_value = MagicMock() + mock_factory.create_session.return_value = mock_session + + strategy = IAMAuthStrategy( + logger=logger, + region="us-east-1", + assume_permissions=True, + ) + + assert strategy._assume_permissions is True diff --git a/tests/unit/providers/aws/test_launch_template_manager.py b/tests/providers/aws/unit/test_launch_template_manager.py similarity index 99% rename from tests/unit/providers/aws/test_launch_template_manager.py rename to tests/providers/aws/unit/test_launch_template_manager.py index 2b16b09e6..ce986cc6b 100644 --- a/tests/unit/providers/aws/test_launch_template_manager.py +++ b/tests/providers/aws/unit/test_launch_template_manager.py @@ -8,6 +8,7 @@ import os import sys +from typing import cast from unittest.mock import Mock import pytest @@ -273,7 +274,7 @@ def test_use_existing_template_strategy_describe_failure_warns_and_passes_throug assert result.template_id == "lt-missing" assert result.is_new_template is False assert result.is_new_version is False - self.manager._logger.warning.assert_called() + cast(Mock, self.manager._logger).warning.assert_called() def test_use_existing_template_strategy_throttling_propagates(self): """Transient errors (Throttling) must propagate, not warn-and-pass-through.""" @@ -333,7 +334,7 @@ def test_inspect_lt_networking_unauthorized_returns_unknown(self): result = self.manager.inspect_launch_template_networking("lt-x", "$Latest") assert result == LTNetworkingState.UNKNOWN_UNAUTHORIZED - self.manager._logger.warning.assert_called() + cast(Mock, self.manager._logger).warning.assert_called() def test_inspect_lt_networking_throttling_propagates(self): """Transient errors (Throttling) must propagate, not return UNKNOWN_UNAUTHORIZED.""" diff --git a/tests/unit/providers/aws/test_native_spec_package_context.py b/tests/providers/aws/unit/test_native_spec_package_context.py similarity index 100% rename from tests/unit/providers/aws/test_native_spec_package_context.py rename to tests/providers/aws/unit/test_native_spec_package_context.py diff --git a/tests/unit/providers/aws/test_registration.py b/tests/providers/aws/unit/test_registration.py similarity index 100% rename from tests/unit/providers/aws/test_registration.py rename to tests/providers/aws/unit/test_registration.py diff --git a/tests/unit/providers/aws/test_spec_file_loading.py b/tests/providers/aws/unit/test_spec_file_loading.py similarity index 100% rename from tests/unit/providers/aws/test_spec_file_loading.py rename to tests/providers/aws/unit/test_spec_file_loading.py diff --git a/tests/unit/providers/aws/test_tag_specifications_guard.py b/tests/providers/aws/unit/test_tag_specifications_guard.py similarity index 100% rename from tests/unit/providers/aws/test_tag_specifications_guard.py rename to tests/providers/aws/unit/test_tag_specifications_guard.py diff --git a/tests/providers/aws/unit/utilities/__init__.py b/tests/providers/aws/unit/utilities/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/providers/aws/unit/utilities/ec2/__init__.py b/tests/providers/aws/unit/utilities/ec2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/providers/aws/utilities/ec2/test_instances_derive_cpu_ram.py b/tests/providers/aws/unit/utilities/ec2/test_instances_derive_cpu_ram.py similarity index 100% rename from tests/unit/providers/aws/utilities/ec2/test_instances_derive_cpu_ram.py rename to tests/providers/aws/unit/utilities/ec2/test_instances_derive_cpu_ram.py diff --git a/tests/providers/contract/base_monitoring_contract.py b/tests/providers/contract/base_monitoring_contract.py index cc1f5c2b8..d1fcdfebe 100644 --- a/tests/providers/contract/base_monitoring_contract.py +++ b/tests/providers/contract/base_monitoring_contract.py @@ -10,6 +10,8 @@ import pytest +from orb.domain.base.provider_fulfilment import CheckHostsStatusResult + class BaseMonitoringContract: """Provider-agnostic monitoring contract scenarios.""" @@ -23,10 +25,13 @@ class BaseMonitoringContract: @pytest.mark.provider_contract def test_status_returns_list(self, provisioned_resource_ids): - """check_hosts_status must return a list.""" + """check_hosts_status must return a CheckHostsStatusResult with an instances list.""" handler, _resource_ids, status_request = provisioned_resource_ids result = handler.check_hosts_status(status_request) - assert isinstance(result, list), "check_hosts_status must return a list" + assert isinstance(result, CheckHostsStatusResult), ( + "check_hosts_status must return a CheckHostsStatusResult" + ) + assert isinstance(result.instances, list), "CheckHostsStatusResult.instances must be a list" @pytest.mark.provider_contract def test_status_entries_have_required_keys(self, provisioned_resource_ids): @@ -35,7 +40,7 @@ def test_status_entries_have_required_keys(self, provisioned_resource_ids): result = handler.check_hosts_status(status_request) # moto simulators may return empty list for some provider APIs (e.g. ASG) # — the contract only asserts shape when entries are present - for entry in result: + for entry in result.instances: assert "instance_id" in entry, f"status entry missing instance_id: {entry}" assert "status" in entry, f"status entry missing status: {entry}" @@ -60,7 +65,7 @@ def test_status_after_release_reflects_termination(self, provisioned_resource_id result = handler.check_hosts_status(status_request) # Either empty (simulator limitation) or all entries show termination state terminal_states = {"terminated", "shutting-down", "stopping", "stopped"} - for entry in result: + for entry in result.instances: assert entry.get("status") in terminal_states, ( f"expected terminal state after release, got: {entry}" ) diff --git a/tests/unit/application/commands/test_return_request_handler_status.py b/tests/unit/application/commands/test_return_request_handler_status.py new file mode 100644 index 000000000..97f8ac3d7 --- /dev/null +++ b/tests/unit/application/commands/test_return_request_handler_status.py @@ -0,0 +1,207 @@ +"""Regression tests: return-request handler must NOT write COMPLETED on termination accept. + +Bug: _execute_deprovisioning_for_request wrote COMPLETED the moment AWS +TerminateInstances returned success. At that point instances are only +shutting-down, not yet terminated. + +Fix: write IN_PROGRESS ("termination accepted: waiting …") so that the +background poller can transition to COMPLETED only when all instances reach +the terminated state. +""" + +from __future__ import annotations + +from contextlib import contextmanager +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from orb.application.commands.request_creation_handlers import CreateReturnRequestHandler +from orb.domain.request.request_types import RequestStatus + +# --------------------------------------------------------------------------- +# Minimal fakes +# --------------------------------------------------------------------------- + + +def _make_uow_factory(machines: list[MagicMock]) -> MagicMock: + uow = MagicMock() + uow.machines.get_by_id.side_effect = lambda mid: next( + (m for m in machines if m.machine_id.value == mid), None + ) + uow.machines.save = MagicMock() + uow.requests.get_by_id.return_value = None + + @contextmanager + def _create(): + yield uow + + factory = MagicMock() + factory.create_unit_of_work.side_effect = _create + return factory + + +def _make_request(request_id: str = "req-001") -> MagicMock: + req = MagicMock() + req.request_id = request_id + req.template_id = "tpl-1" + req.provider_name = "aws" + req.provider_type = "aws" + return req + + +def _make_machine(machine_id: str) -> MagicMock: + m = MagicMock() + m.machine_id = MagicMock() + m.machine_id.value = machine_id + m.template_id = "tpl-1" + m.request_id = "req-origin-001" + m.update_status.return_value = m + m.model_copy.return_value = m + return m + + +def _make_handler() -> tuple[CreateReturnRequestHandler, MagicMock]: + """Return (handler, command_bus_mock).""" + logger = MagicMock() + container = MagicMock() + event_publisher = MagicMock() + error_handler = MagicMock() + query_bus = MagicMock() + query_bus.execute = AsyncMock() + provider_selection_port = MagicMock() + provider_selection_port.execute_operation = AsyncMock() + + uow_factory = _make_uow_factory([_make_machine("i-aaa"), _make_machine("i-bbb")]) + + # Command bus is obtained via container.get(CommandBusPort) + command_bus = MagicMock() + command_bus.execute = AsyncMock() + container.get = MagicMock(return_value=command_bus) + + handler = CreateReturnRequestHandler( + uow_factory=uow_factory, + logger=logger, + container=container, + event_publisher=event_publisher, + error_handler=error_handler, + query_bus=query_bus, + provider_selection_port=provider_selection_port, + ) + return handler, command_bus + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +@pytest.mark.application +class TestReturnRequestHandlerStatus: + """Verify that termination-accepted path writes IN_PROGRESS, not COMPLETED.""" + + @pytest.mark.asyncio + async def test_termination_accepted_writes_in_progress_not_completed(self): + """Core regression: handler must write IN_PROGRESS when deprovisioning succeeds.""" + handler, command_bus = _make_handler() + request = _make_request() + + # Patch deprovisioning orchestrator to simulate successful termination accept + handler._deprovisioning_orchestrator.execute_deprovisioning = AsyncMock( + return_value={"success": True, "successful_operations": 1, "failed_operations": 0} + ) + # Patch machine grouping to return one group, no skipped IDs + handler._machine_grouping_service.group_by_resource = MagicMock( + return_value=({("aws", "RunInstances", "fleet-1"): [MagicMock()]}, []) + ) + + await handler._execute_deprovisioning_for_request(["i-aaa", "i-bbb"], request, "aws") + + # Gather all UpdateRequestStatusCommand calls + from orb.application.dto.commands import UpdateRequestStatusCommand + + status_calls = [ + c + for c in command_bus.execute.call_args_list + if isinstance(c.args[0], UpdateRequestStatusCommand) + ] + + statuses_written = [c.args[0].status for c in status_calls] + + # Must NOT contain COMPLETED + assert RequestStatus.COMPLETED not in statuses_written, ( + "Handler wrote COMPLETED immediately on termination accept — bug is not fixed" + ) + + # Must contain IN_PROGRESS (either from the initial transition or from the + # termination-accepted path) + assert RequestStatus.IN_PROGRESS in statuses_written, ( + "Handler did not write IN_PROGRESS after termination was accepted" + ) + + @pytest.mark.asyncio + async def test_termination_accepted_message_is_honest(self): + """Status message must not say 'termination initiated' as if it's done.""" + handler, command_bus = _make_handler() + request = _make_request() + + handler._deprovisioning_orchestrator.execute_deprovisioning = AsyncMock( + return_value={"success": True, "successful_operations": 1, "failed_operations": 0} + ) + handler._machine_grouping_service.group_by_resource = MagicMock( + return_value=({("aws", "RunInstances", "fleet-1"): [MagicMock()]}, []) + ) + + await handler._execute_deprovisioning_for_request(["i-aaa"], request, "aws") + + from orb.application.dto.commands import UpdateRequestStatusCommand + + status_calls = [ + c + for c in command_bus.execute.call_args_list + if isinstance(c.args[0], UpdateRequestStatusCommand) + and c.args[0].status == RequestStatus.IN_PROGRESS + ] + messages = [c.args[0].message for c in status_calls] + + # At least one IN_PROGRESS message must be about "waiting" or "accepted" + honest_messages = [ + m + for m in messages + if "waiting" in m.lower() or "accepted" in m.lower() or "terminating" in m.lower() + ] + assert honest_messages, ( + f"No honest termination-in-progress message found. Messages: {messages}" + ) + + @pytest.mark.asyncio + async def test_deprovisioning_failure_writes_failed_not_completed(self): + """On deprovisioning failure, write FAILED, never COMPLETED.""" + handler, command_bus = _make_handler() + request = _make_request() + + handler._deprovisioning_orchestrator.execute_deprovisioning = AsyncMock( + return_value={ + "success": False, + "errors": ["TerminateInstances throttled"], + "failed_operations": 1, + } + ) + handler._machine_grouping_service.group_by_resource = MagicMock( + return_value=({("aws", "RunInstances", "fleet-1"): [MagicMock()]}, []) + ) + + await handler._execute_deprovisioning_for_request(["i-aaa"], request, "aws") + + from orb.application.dto.commands import UpdateRequestStatusCommand + + status_calls = [ + c + for c in command_bus.execute.call_args_list + if isinstance(c.args[0], UpdateRequestStatusCommand) + ] + statuses_written = [c.args[0].status for c in status_calls] + + assert RequestStatus.COMPLETED not in statuses_written + assert RequestStatus.FAILED in statuses_written diff --git a/tests/unit/application/queries/test_get_request_handler.py b/tests/unit/application/queries/test_get_request_handler.py index 3621f3a92..04b8035ef 100644 --- a/tests/unit/application/queries/test_get_request_handler.py +++ b/tests/unit/application/queries/test_get_request_handler.py @@ -146,13 +146,25 @@ async def test_get_request_falls_back_to_stored_state_on_sync_error(): @pytest.mark.asyncio async def test_get_request_returns_synced_dto_on_success(): - """Normal (no-error) path returns a DTO and writes to cache.""" - request = _make_request(_ID_SUCCESS) + """Normal (no-error) path returns a DTO; cache is written when request transitions to terminal. - handler, mock_query_service, mock_cache_service = _make_handler(request, sync_side_effect=None) + Sequence under test: + 1. First get_request() call → IN_PROGRESS (not short-circuited, sync runs). + 2. Sync drives the status to COMPLETED. + 3. Second get_request() call after sync → COMPLETED. + 4. request.status.is_terminal() is True → cache_request() is called once. + """ + in_progress_request = _make_request(_ID_SUCCESS) + in_progress_request = in_progress_request.update_status(RequestStatus.IN_PROGRESS, "syncing") + completed_request = in_progress_request.update_status(RequestStatus.COMPLETED, "done") - # After sync the handler re-fetches the request; return the same object for simplicity - mock_query_service.get_request = AsyncMock(return_value=request) + handler, mock_query_service, mock_cache_service = _make_handler( + in_progress_request, sync_side_effect=None + ) + + # First call returns IN_PROGRESS so the handler takes the sync path. + # Second call (after status update) returns COMPLETED so the cache is written. + mock_query_service.get_request = AsyncMock(side_effect=[in_progress_request, completed_request]) query = GetRequestQuery(request_id=_ID_SUCCESS) result = await handler.execute_query(query) @@ -160,10 +172,33 @@ async def test_get_request_returns_synced_dto_on_success(): assert isinstance(result, RequestDTO) assert result.request_id == _ID_SUCCESS - # Cache SHOULD be written on the success path + # Cache SHOULD be written once the request reaches a terminal state mock_cache_service.cache_request.assert_called_once() +@pytest.mark.asyncio +async def test_get_request_does_not_cache_non_terminal(): + """Non-terminal requests (PENDING, IN_PROGRESS) are never cached. + + Caching non-terminal states prevents the sync loop from observing provider + state transitions and keeps the request stuck at the cached status. + """ + pending_request = _make_request(_ID_SUCCESS) # PENDING by default + + handler, mock_query_service, mock_cache_service = _make_handler( + pending_request, sync_side_effect=None + ) + + mock_query_service.get_request = AsyncMock(return_value=pending_request) + + query = GetRequestQuery(request_id=_ID_SUCCESS) + result = await handler.execute_query(query) + + assert isinstance(result, RequestDTO) + # Non-terminal → cache_request must NOT be called + mock_cache_service.cache_request.assert_not_called() + + @pytest.mark.asyncio async def test_get_request_raises_entity_not_found_when_missing(): """EntityNotFoundError (request missing) propagates — no swallowing.""" diff --git a/tests/unit/application/queries/test_list_requests_filter_by_type.py b/tests/unit/application/queries/test_list_requests_filter_by_type.py index 603eaadc0..43f577afe 100644 --- a/tests/unit/application/queries/test_list_requests_filter_by_type.py +++ b/tests/unit/application/queries/test_list_requests_filter_by_type.py @@ -82,15 +82,10 @@ def _run_handler_with_requests(all_requests, query): mock_uow_factory = MagicMock() mock_uow_factory.create_unit_of_work.return_value = mock_uow - handler = ListRequestsHandler( - uow_factory=mock_uow_factory, - logger=mock_logger, - error_handler=mock_error_handler, - generic_filter_service=mock_filter_service, - ) - - # Patch RequestDTOFactory so we don't need full domain wiring - with patch("orb.application.factories.request_dto_factory.RequestDTOFactory") as MockFactory: + # Patch RequestDTOFactory at the import site so we don't need full domain wiring. + # Patch must wrap handler construction because the handler eagerly instantiates + # self._dto_factory = RequestDTOFactory() in __init__. + with patch("orb.application.queries.request_query_handlers.RequestDTOFactory") as MockFactory: mock_dto_factory = MagicMock() MockFactory.return_value = mock_dto_factory mock_dto_factory.create_from_domain.side_effect = lambda req, machines: SimpleNamespace( @@ -98,6 +93,13 @@ def _run_handler_with_requests(all_requests, query): request_type=req.request_type.value, ) + handler = ListRequestsHandler( + uow_factory=mock_uow_factory, + logger=mock_logger, + error_handler=mock_error_handler, + generic_filter_service=mock_filter_service, + ) + import asyncio return asyncio.run(handler.execute_query(query)) diff --git a/tests/unit/application/queries/test_request_status_capacity.py b/tests/unit/application/queries/test_request_status_capacity.py index 0f1be0225..34774fb3a 100644 --- a/tests/unit/application/queries/test_request_status_capacity.py +++ b/tests/unit/application/queries/test_request_status_capacity.py @@ -6,6 +6,7 @@ import pytest from orb.application.services.request_status_service import RequestStatusService +from orb.domain.base.provider_fulfilment import ProviderFulfilment from orb.domain.base.value_objects import InstanceType from orb.domain.machine.aggregate import Machine from orb.domain.machine.machine_identifiers import MachineId @@ -63,8 +64,17 @@ def test_fleet_capacity_completed_even_with_few_instances(): service = _make_service() request = _request(RequestType.ACQUIRE, RequestStatus.IN_PROGRESS, requested_count=10) machines = _machines(MachineStatus.RUNNING, MachineStatus.RUNNING) - - new_status, _ = service.determine_status_from_machines([], machines, request, {}) + # 2 running out of 10 requested — provider reports in_progress + fulfilment = ProviderFulfilment( + state="in_progress", + message="2 of 10 running, still provisioning", + target_units=10, + fulfilled_units=2, + running_count=2, + ) + new_status, _ = service.determine_status_from_machines( + [], machines, request, {"provider_fulfilment": fulfilment} + ) # 2 running out of 10 requested — partial or in-progress, not completed assert new_status in ( RequestStatus.IN_PROGRESS.value, @@ -79,8 +89,16 @@ def test_fleet_capacity_in_progress_when_under_target(): request = _request(RequestType.ACQUIRE, RequestStatus.IN_PROGRESS, requested_count=10) # All pending — running=0, failed=0, so returns IN_PROGRESS machines = _machines(MachineStatus.PENDING, MachineStatus.PENDING) - - new_status, _ = service.determine_status_from_machines([], machines, request, {}) + fulfilment = ProviderFulfilment( + state="in_progress", + message="2 of 10 pending", + target_units=10, + fulfilled_units=0, + pending_count=2, + ) + new_status, _ = service.determine_status_from_machines( + [], machines, request, {"provider_fulfilment": fulfilment} + ) assert new_status == RequestStatus.IN_PROGRESS.value @@ -95,8 +113,17 @@ def test_fleet_capacity_partial_with_failures(): MachineStatus.FAILED, MachineStatus.FAILED, ) - - new_status, _ = service.determine_status_from_machines([], machines, request, {}) + fulfilment = ProviderFulfilment( + state="partial", + message="2 running, 3 failed — partial fulfilment", + target_units=5, + fulfilled_units=2, + running_count=2, + failed_count=3, + ) + new_status, _ = service.determine_status_from_machines( + [], machines, request, {"provider_fulfilment": fulfilment} + ) assert new_status == RequestStatus.PARTIAL.value @@ -110,8 +137,16 @@ def test_fleet_capacity_all_failed(): MachineStatus.FAILED, MachineStatus.FAILED, ) - - new_status, _ = service.determine_status_from_machines([], machines, request, {}) + fulfilment = ProviderFulfilment( + state="failed", + message="All 4 instances failed", + target_units=4, + fulfilled_units=0, + failed_count=4, + ) + new_status, _ = service.determine_status_from_machines( + [], machines, request, {"provider_fulfilment": fulfilment} + ) assert new_status == RequestStatus.FAILED.value @@ -120,8 +155,16 @@ def test_asg_capacity_completed(): service = _make_service() request = _request(RequestType.ACQUIRE, RequestStatus.IN_PROGRESS, requested_count=1) machines = _machines(MachineStatus.RUNNING) - - new_status, _ = service.determine_status_from_machines([], machines, request, {}) + fulfilment = ProviderFulfilment( + state="fulfilled", + message="1 of 1 running", + target_units=1, + fulfilled_units=1, + running_count=1, + ) + new_status, _ = service.determine_status_from_machines( + [], machines, request, {"provider_fulfilment": fulfilment} + ) assert new_status == RequestStatus.COMPLETED.value @@ -131,8 +174,16 @@ def test_asg_capacity_in_progress(): request = _request(RequestType.ACQUIRE, RequestStatus.IN_PROGRESS, requested_count=6) # All pending — running=0, failed=0, so returns IN_PROGRESS machines = _machines(MachineStatus.PENDING, MachineStatus.PENDING) - - new_status, _ = service.determine_status_from_machines([], machines, request, {}) + fulfilment = ProviderFulfilment( + state="in_progress", + message="2 pending, waiting for target of 6", + target_units=6, + fulfilled_units=0, + pending_count=2, + ) + new_status, _ = service.determine_status_from_machines( + [], machines, request, {"provider_fulfilment": fulfilment} + ) assert new_status == RequestStatus.IN_PROGRESS.value @@ -141,8 +192,16 @@ def test_runinstances_completed_without_capacity_metadata(): service = _make_service() request = _request(RequestType.ACQUIRE, RequestStatus.IN_PROGRESS, requested_count=2) machines = _machines(MachineStatus.RUNNING, MachineStatus.RUNNING) - - new_status, msg = service.determine_status_from_machines([], machines, request, {}) + fulfilment = ProviderFulfilment( + state="fulfilled", + message="2 of 2 running", + target_units=2, + fulfilled_units=2, + running_count=2, + ) + new_status, msg = service.determine_status_from_machines( + [], machines, request, {"provider_fulfilment": fulfilment} + ) assert new_status == RequestStatus.COMPLETED.value assert msg is not None assert "running" in msg.lower() @@ -155,8 +214,16 @@ def test_runinstances_timeout_when_no_instances_after_long_time(): request = _request( RequestType.ACQUIRE, RequestStatus.IN_PROGRESS, requested_count=1, created_at=old_time ) - - new_status, msg = service.determine_status_from_machines([], [], request, {}) + # No machines yet — provider still reports in_progress (keep polling) + fulfilment = ProviderFulfilment( + state="in_progress", + message="No instances yet after 31 minutes", + target_units=1, + fulfilled_units=0, + ) + new_status, _msg = service.determine_status_from_machines( + [], [], request, {"provider_fulfilment": fulfilment} + ) # No machines yet — service returns IN_PROGRESS (keep polling) assert new_status == RequestStatus.IN_PROGRESS.value @@ -164,7 +231,8 @@ def test_runinstances_timeout_when_no_instances_after_long_time(): @pytest.mark.unit def test_return_request_completed_when_all_terminated(): service = _make_service() - request = _request(RequestType.RETURN, RequestStatus.IN_PROGRESS, requested_count=3) + # requested_count matches the number of terminated machines provided + request = _request(RequestType.RETURN, RequestStatus.IN_PROGRESS, requested_count=2) machines = _machines(MachineStatus.TERMINATED, MachineStatus.TERMINATED) new_status, _ = service.determine_status_from_machines([], machines, request, {}) @@ -193,7 +261,15 @@ def test_provisioning_failure_metadata_forces_failed(): "error_message": "Failed to create EC2 fleet", }, ) - + # Provider still reports in_progress — application polls until provider says failed + fulfilment = ProviderFulfilment( + state="in_progress", + message="Provisioning in progress", + target_units=3, + fulfilled_units=0, + ) # No machines, no provider machines — service returns IN_PROGRESS (keep polling) - new_status, _ = service.determine_status_from_machines([], [], request, {}) + new_status, _ = service.determine_status_from_machines( + [], [], request, {"provider_fulfilment": fulfilment} + ) assert new_status == RequestStatus.IN_PROGRESS.value diff --git a/tests/unit/application/services/orchestration/test_return_machines_orchestrator.py b/tests/unit/application/services/orchestration/test_return_machines_orchestrator.py index 404d08dc5..e65c8c5fa 100644 --- a/tests/unit/application/services/orchestration/test_return_machines_orchestrator.py +++ b/tests/unit/application/services/orchestration/test_return_machines_orchestrator.py @@ -189,3 +189,50 @@ async def _set_request_ids(cmd): result = await orchestrator.execute(input) assert result.status == "pending" assert result.request_id == "ret-req-001" + + @pytest.mark.asyncio + async def test_output_machine_ids_populated_from_explicit_input( + self, orchestrator, mock_command_bus + ): + """machine_ids in output must reflect the machines submitted for return.""" + + async def _set_request_ids(cmd): + cmd.created_request_ids = ["ret-req-001"] + + mock_command_bus.execute.side_effect = _set_request_ids + input = ReturnMachinesInput(machine_ids=["i-aaa", "i-bbb", "i-ccc"]) + result = await orchestrator.execute(input) + assert isinstance(result, ReturnMachinesOutput) + assert result.machine_ids == ["i-aaa", "i-bbb", "i-ccc"] + + @pytest.mark.asyncio + async def test_output_machine_ids_populated_from_all_machines_path( + self, orchestrator, mock_query_bus, mock_command_bus + ): + """machine_ids in output must reflect resolved IDs from --all path.""" + mock_query_bus.execute.return_value = [ + MagicMock(machine_id="i-001"), + MagicMock(machine_id="i-002"), + ] + + async def _set_request_ids(cmd): + cmd.created_request_ids = ["ret-req-001"] + + mock_command_bus.execute.side_effect = _set_request_ids + input = ReturnMachinesInput(all_machines=True) + result = await orchestrator.execute(input) + assert sorted(result.machine_ids) == ["i-001", "i-002"] + + @pytest.mark.asyncio + async def test_output_machine_ids_empty_when_no_op(self, orchestrator, mock_command_bus): + """machine_ids must be empty (default) when no request was created.""" + + async def _set_empty(cmd): + cmd.created_request_ids = [] + cmd.skipped_machines = ["i-001"] + + mock_command_bus.execute.side_effect = _set_empty + input = ReturnMachinesInput(machine_ids=["i-001"]) + result = await orchestrator.execute(input) + assert result.status == "no_op" + assert result.machine_ids == [] diff --git a/tests/unit/application/services/test_machine_sync_service.py b/tests/unit/application/services/test_machine_sync_service.py new file mode 100644 index 000000000..c067e4902 --- /dev/null +++ b/tests/unit/application/services/test_machine_sync_service.py @@ -0,0 +1,145 @@ +"""Unit tests for MachineSyncService — basic behaviour and OperationOutcome awareness. + +The sync service does not directly call acquire/return_machines/get_status; it uses +execute_operation via ProviderRegistryService. These tests cover the core paths +that interact with the outcome-aware request status logic. +""" + +from unittest.mock import MagicMock + +import pytest + +from orb.application.services.machine_sync_service import MachineSyncService +from orb.domain.machine.machine_status import MachineStatus + + +def _make_service() -> MachineSyncService: + command_bus = MagicMock() + uow_factory = MagicMock() + config_port = MagicMock() + logger = MagicMock() + return MachineSyncService( + command_bus=command_bus, + uow_factory=uow_factory, + config_port=config_port, + logger=logger, + ) + + +def _make_request(request_type: str = "acquire", provider_api: str = "RunInstances"): + req = MagicMock() + req.request_id = MagicMock() + req.request_id.__str__ = lambda self: "req-sync-test" + req.request_type.value = request_type + req.provider_name = "aws_default_us-east-1" + req.provider_api = provider_api + req.template_id = "tmpl-1" + req.resource_ids = ["fleet-1"] + req.machine_ids = [] + req.metadata = {} + return req + + +def _make_machine(mid: str, status: MachineStatus = MachineStatus.RUNNING): + m = MagicMock() + m.machine_id.value = mid + m.status = status + return m + + +@pytest.mark.unit +class TestFetchProviderMachinesNoRegistryService: + """fetch_provider_machines returns (db_machines, {}) when no registry service.""" + + @pytest.mark.asyncio + async def test_returns_db_machines_when_registry_service_missing(self): + svc = _make_service() + db_machines = [_make_machine("i-1")] + req = _make_request() + + # No provider_registry_service injected → should raise RuntimeError + # which is caught internally and returns (db_machines, {}) + machines, meta = await svc.fetch_provider_machines(req, db_machines) # type: ignore[arg-type] + assert machines == db_machines + assert meta == {} + + +@pytest.mark.unit +class TestFetchProviderMachinesEmpty: + """fetch_provider_machines returns ([], {}) when no machine context.""" + + @pytest.mark.asyncio + async def test_no_resource_ids_and_no_db_machines_returns_empty(self): + svc = _make_service() + req = _make_request() + req.resource_ids = [] + req.machine_ids = [] + + machines, meta = await svc.fetch_provider_machines(req, []) + assert machines == [] + assert meta == {} + + +@pytest.mark.unit +class TestFetchProviderMachinesWithMockRegistry: + """fetch_provider_machines propagates provider instance data.""" + + @pytest.mark.asyncio + async def test_acquire_path_calls_registry_service(self): + """fetch_provider_machines calls the registry service for acquire requests.""" + from orb.providers.base.strategy.provider_strategy import ProviderResult + + svc = _make_service() + + captured: list = [] + + async def _capture(provider_name, operation): + captured.append(operation.operation_type.value) + return ProviderResult.success_result(data={"instances": []}) + + registry_svc = MagicMock() + registry_svc.execute_operation = _capture + svc._provider_registry_service = registry_svc + + req = _make_request(request_type="acquire") + db_machines: list = [] + + machines, _meta = await svc.fetch_provider_machines(req, db_machines) + # The registry was called and returned empty instances → machines is empty + assert "describe_resource_instances" in captured + assert machines == [] + + @pytest.mark.asyncio + async def test_return_path_uses_instance_status_operation(self): + """For return requests with machine_ids, the GET_INSTANCE_STATUS operation is used.""" + from orb.providers.base.strategy.provider_strategy import ProviderResult + + svc = _make_service() + + called_operations: list = [] + + async def _capture(provider_name, operation): + called_operations.append(operation.operation_type.value) + return ProviderResult.success_result( + data={ + "instances": [ + { + "instance_id": "i-1", + "status": "shutting-down", + "instance_type": "m5.large", + } + ] + }, + ) + + registry_svc = MagicMock() + registry_svc.execute_operation = _capture + svc._provider_registry_service = registry_svc + + req = _make_request(request_type="return") + req.machine_ids = ["i-1"] + + db_machines = [_make_machine("i-1", MachineStatus.RUNNING)] + await svc.fetch_provider_machines(req, db_machines) # type: ignore[arg-type] + + assert "get_instance_status" in called_operations diff --git a/tests/unit/application/services/test_provisioning_async_guards.py b/tests/unit/application/services/test_provisioning_async_guards.py new file mode 100644 index 000000000..db3256fa1 --- /dev/null +++ b/tests/unit/application/services/test_provisioning_async_guards.py @@ -0,0 +1,340 @@ +"""Tests for async correctness guards in ProvisioningOrchestrationService. + +Covers: +- asyncio.timeout wrapping the execute_operation call in _dispatch_single_attempt +- asyncio.to_thread offloading _persist_acquiring to a worker thread +""" + +import asyncio +from typing import cast +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from orb.application.services.provisioning_orchestration_service import ( + ProvisioningOrchestrationService, +) +from orb.domain.base.results import ProviderSelectionResult + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_service(dispatch_timeout: float = 10.0) -> ProvisioningOrchestrationService: + """Build a service with all mocks wired; dispatch_timeout_seconds from config.""" + container = MagicMock() + logger = MagicMock() + provider_selection_port = MagicMock() + provider_config_port = MagicMock() + config_port = MagicMock() + circuit_breaker_factory = MagicMock() + + config_port.get_request_config.return_value = { + "dispatch_timeout_seconds": dispatch_timeout, + } + + cb = MagicMock() + cb.has_state.return_value = False + circuit_breaker_factory.return_value = cb + + return ProvisioningOrchestrationService( + container=container, + logger=logger, + provider_selection_port=provider_selection_port, + provider_config_port=provider_config_port, + config_port=config_port, + circuit_breaker_factory=circuit_breaker_factory, + ) + + +def _make_request(count: int = 1): + request = MagicMock() + request.request_id = "req-timeout-test" + request.requested_count = count + request.metadata = {} + request.update_metadata = lambda d: request + return request + + +def _make_template(): + template = MagicMock() + template.template_id = "tmpl-timeout" + return template + + +def _make_selection_result(provider_name: str = "aws_default_us-east-1") -> ProviderSelectionResult: + return ProviderSelectionResult( + provider_name=provider_name, + provider_type="aws", + selection_reason="test", + confidence=1.0, + ) + + +# --------------------------------------------------------------------------- +# asyncio.timeout tests +# --------------------------------------------------------------------------- + + +class TestDispatchTimeout: + """_dispatch_single_attempt must time out when the operation hangs.""" + + @pytest.mark.asyncio + async def test_timeout_returns_failure_result(self): + """When execute_operation hangs beyond the timeout, a failed ProvisioningResult is returned.""" + svc = _make_service(dispatch_timeout=0.05) # 50 ms — fast for tests + + # Simulate a hung provider: never resolves + async def _hang(*_args, **_kwargs): + await asyncio.sleep(60) # much longer than the 50 ms timeout + + svc._provider_selection_port.execute_operation = _hang + + scheduler = MagicMock() + scheduler.format_template_for_provider.return_value = {} + cast(MagicMock, svc._container).get.return_value = scheduler + + result = await svc._dispatch_single_attempt( + _make_template(), + _make_request(), + _make_selection_result(), + count=1, + dispatch_timeout_seconds=0.05, + ) + + assert result.success is False + assert result.is_final is True + assert "timed out" in (result.error_message or "").lower() + + @pytest.mark.asyncio + async def test_timeout_logs_warning(self): + """A warning is logged when the dispatch times out.""" + svc = _make_service(dispatch_timeout=0.05) + + async def _hang(*_args, **_kwargs): + await asyncio.sleep(60) + + svc._provider_selection_port.execute_operation = _hang + + scheduler = MagicMock() + scheduler.format_template_for_provider.return_value = {} + cast(MagicMock, svc._container).get.return_value = scheduler + + await svc._dispatch_single_attempt( + _make_template(), + _make_request(), + _make_selection_result(), + count=1, + dispatch_timeout_seconds=0.05, + ) + + cast(MagicMock, svc._logger).warning.assert_called() + warning_call_args = str(cast(MagicMock, svc._logger).warning.call_args_list) + assert "timed out" in warning_call_args.lower() or "timeout" in warning_call_args.lower() + + @pytest.mark.asyncio + async def test_fast_operation_completes_normally(self): + """An operation that completes within the timeout is not interrupted.""" + from orb.providers.base.strategy.provider_strategy import ProviderResult + + svc = _make_service(dispatch_timeout=10.0) + + provider_result = ProviderResult.success_result( + data={ + "resource_ids": ["i-ok"], + "instances": [{"id": "i-ok"}], + "instance_ids": ["i-ok"], + }, + metadata={}, + ) + svc._provider_selection_port.execute_operation = AsyncMock(return_value=provider_result) + + scheduler = MagicMock() + scheduler.format_template_for_provider.return_value = {} + cast(MagicMock, svc._container).get.return_value = scheduler + + result = await svc._dispatch_single_attempt( + _make_template(), + _make_request(), + _make_selection_result(), + count=1, + dispatch_timeout_seconds=10.0, + ) + + assert result.success is True + assert result.resource_ids == ["i-ok"] + + @pytest.mark.asyncio + async def test_timeout_fires_via_execute_provisioning_config(self): + """dispatch_timeout_seconds from config flows through to the dispatch call.""" + svc = _make_service(dispatch_timeout=0.05) + + call_count = 0 + + async def _hang(*_args, **_kwargs): + nonlocal call_count + call_count += 1 + await asyncio.sleep(60) + + svc._provider_selection_port.execute_operation = _hang + + scheduler = MagicMock() + scheduler.format_template_for_provider.return_value = {} + cast(MagicMock, svc._container).get.return_value = scheduler + + request = _make_request(count=1) + # Make update_metadata return a fresh mock that also has metadata={} and matching attrs + updated_req = MagicMock() + updated_req.request_id = "req-timeout-test" + updated_req.requested_count = 1 + updated_req.metadata = {} + updated_req.update_metadata = lambda d: updated_req + request.update_metadata = lambda d: updated_req + + result = await svc.execute_provisioning(_make_template(), request, _make_selection_result()) + + # At least one attempt was made and resulted in failure due to timeout + assert call_count >= 1 + assert result.success is False + + +# --------------------------------------------------------------------------- +# asyncio.to_thread tests +# --------------------------------------------------------------------------- + + +class TestPersistAcquiringToThread: + """_persist_acquiring must run in a worker thread via asyncio.to_thread.""" + + @pytest.mark.asyncio + async def test_persist_acquiring_called_via_to_thread(self): + """asyncio.to_thread is used when calling _persist_acquiring.""" + from orb.providers.base.strategy.provider_strategy import ProviderResult + + svc = _make_service(dispatch_timeout=10.0) + + # First attempt: partial (is_final=False) — triggers persist_acquiring + # Second attempt: fully fulfilled + first_result = ProviderResult.success_result( + data={ + "resource_ids": ["i-partial"], + "instances": [{"id": "i-partial"}], + "instance_ids": ["i-partial"], + }, + metadata={}, + ) + second_result = ProviderResult.success_result( + data={ + "resource_ids": ["i-rest"], + "instances": [{"id": "i-rest"}], + "instance_ids": ["i-rest"], + }, + metadata={}, + ) + svc._provider_selection_port.execute_operation = AsyncMock( + side_effect=[first_result, second_result] + ) + + scheduler = MagicMock() + scheduler.format_template_for_provider.return_value = {} + cast(MagicMock, svc._container).get.return_value = scheduler + + to_thread_calls: list = [] + + original_to_thread = asyncio.to_thread + + async def _spy_to_thread(func, *args, **kwargs): + to_thread_calls.append(func) + return await original_to_thread(func, *args, **kwargs) + + # Build a request that needs 2 instances so a second attempt is triggered + request = MagicMock() + request.request_id = "req-persist-test" + request.requested_count = 2 + request.metadata = {} + + call_counter = [0] + + def _update_metadata(d): + call_counter[0] += 1 + return request + + request.update_metadata = _update_metadata + + # _persist_acquiring needs update_status + request.update_status = MagicMock(return_value=request) + + # Patch asyncio.to_thread in the module under test + with patch( + "orb.application.services.provisioning_orchestration_service.asyncio.to_thread", + side_effect=_spy_to_thread, + ): + # We don't need the full loop to run — just verify to_thread is called + # by exercising the partial-fulfillment path + await svc.execute_provisioning(_make_template(), request, _make_selection_result()) + + # _persist_acquiring should have been scheduled via to_thread + persist_calls = [f for f in to_thread_calls if f == svc._persist_acquiring] + assert len(persist_calls) >= 1, ( + "_persist_acquiring was not dispatched via asyncio.to_thread" + ) + + @pytest.mark.asyncio + async def test_persist_acquiring_failure_does_not_abort_loop(self): + """If _persist_acquiring raises, the retry loop continues with in-memory state.""" + from orb.providers.base.strategy.provider_strategy import ProviderResult + + svc = _make_service(dispatch_timeout=10.0) + + # Two partial attempts, then a final one + partial_result = ProviderResult.success_result( + data={ + "resource_ids": ["i-p"], + "instances": [{"id": "i-p"}], + "instance_ids": ["i-p"], + }, + metadata={}, + ) + final_result = ProviderResult.success_result( + data={ + "resource_ids": ["i-f"], + "instances": [{"id": "i-f"}], + "instance_ids": ["i-f"], + }, + metadata={}, + ) + svc._provider_selection_port.execute_operation = AsyncMock( + side_effect=[partial_result, final_result] + ) + + scheduler = MagicMock() + scheduler.format_template_for_provider.return_value = {} + cast(MagicMock, svc._container).get.return_value = scheduler + + request = MagicMock() + request.request_id = "req-persist-fail" + request.requested_count = 2 + request.metadata = {} + request.update_metadata = lambda d: request + request.update_status = MagicMock(return_value=request) + + # Make _persist_acquiring return failure (second return value = False) + def _failing_persist(req): + return req, False + + svc._persist_acquiring = _failing_persist # type: ignore[method-assign] + + async def _inline_to_thread(func, *args, **kwargs): + return func(*args, **kwargs) + + with patch( + "orb.application.services.provisioning_orchestration_service.asyncio.to_thread", + side_effect=_inline_to_thread, + ): + await svc.execute_provisioning(_make_template(), request, _make_selection_result()) + + # Loop should have continued despite persist failure + cast(MagicMock, svc._logger).warning.assert_called() + warning_msgs = " ".join(str(c) for c in cast(MagicMock, svc._logger).warning.call_args_list) + assert "ACQUIRING persist failed" in warning_msgs or "persist" in warning_msgs.lower() diff --git a/tests/unit/application/services/test_provisioning_orchestration_service.py b/tests/unit/application/services/test_provisioning_orchestration_service.py new file mode 100644 index 000000000..ebd30f2d2 --- /dev/null +++ b/tests/unit/application/services/test_provisioning_orchestration_service.py @@ -0,0 +1,317 @@ +"""Unit tests for ProvisioningOrchestrationService — OperationOutcome integration. + +Covers: +- ProvisioningResult.outcome is set for all return paths +- ProvisioningResult.is_final is correctly derived from outcome via __post_init__ +- assert_never exhaustiveness compiles (no missed union branches) +""" + +from typing import assert_never +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from orb.application.services.provisioning_orchestration_service import ( + ProvisioningOrchestrationService, + ProvisioningResult, +) +from orb.domain.base.operation_outcome import ( + Accepted, + Completed, + Failed, + OperationOutcome, + RequiresFollowUp, +) +from orb.domain.base.results import ProviderSelectionResult + +# --------------------------------------------------------------------------- +# ProvisioningResult — outcome/is_final derivation +# --------------------------------------------------------------------------- + + +class TestProvisioningResultOutcomeDerivation: + """is_final must be derived from outcome when outcome is present.""" + + def test_accepted_sets_is_final_false(self): + result = ProvisioningResult( + success=True, + resource_ids=["r-1"], + machine_ids=[], + instances=[], + provider_data={}, + outcome=Accepted(request_id="req-1", pending_resource_ids=["r-1"]), + ) + assert result.is_final is False + + def test_completed_sets_is_final_true(self): + result = ProvisioningResult( + success=True, + resource_ids=["r-1"], + machine_ids=["i-1"], + instances=[{"id": "i-1"}], + provider_data={}, + outcome=Completed(resource_ids=["r-1"]), + ) + assert result.is_final is True + + def test_failed_sets_is_final_true(self): + result = ProvisioningResult( + success=False, + resource_ids=[], + machine_ids=[], + instances=[], + provider_data={}, + outcome=Failed(error="something went wrong"), + ) + assert result.is_final is True + + def test_recoverable_failed_sets_is_final_true(self): + result = ProvisioningResult( + success=False, + resource_ids=[], + machine_ids=[], + instances=[], + provider_data={}, + outcome=Failed(error="timeout", recoverable=True), + ) + assert result.is_final is True + + def test_requires_follow_up_sets_is_final_false(self): + from orb.application.services.request_follow_up_context import ( + TerminationFollowUpContext, + ) + + ctx = TerminationFollowUpContext(pending_instance_ids=["i-1"]) + result = ProvisioningResult( + success=True, + resource_ids=[], + machine_ids=[], + instances=[], + provider_data={}, + outcome=RequiresFollowUp(context=ctx), + ) + assert result.is_final is False + + def test_no_outcome_honours_explicit_is_final_true(self): + """Legacy path: when outcome is None, explicit is_final is preserved.""" + result = ProvisioningResult( + success=True, + resource_ids=[], + machine_ids=[], + instances=[], + provider_data={}, + is_final=True, + ) + assert result.is_final is True + + def test_no_outcome_honours_explicit_is_final_false(self): + """Legacy path: when outcome is None, explicit is_final=False is preserved.""" + result = ProvisioningResult( + success=True, + resource_ids=[], + machine_ids=[], + instances=[], + provider_data={}, + is_final=False, + ) + assert result.is_final is False + + +# --------------------------------------------------------------------------- +# OperationOutcome exhaustiveness — assert_never +# --------------------------------------------------------------------------- + + +class TestOperationOutcomeExhaustiveness: + """assert_never must cover all union branches without static errors.""" + + def test_all_union_branches_handled(self): + """Exhaustive match over all OperationOutcome variants must not raise.""" + outcomes: list[OperationOutcome] = [ + Accepted(request_id="req-1"), + Completed(resource_ids=["r-1"]), + Failed(error="oops"), + ] + results = [] + for outcome in outcomes: + match outcome: + case Accepted(request_id=rid): + results.append(f"accepted:{rid}") + case Completed(resource_ids=ids): + results.append(f"completed:{ids}") + case RequiresFollowUp(): + results.append("follow_up") + case Failed(error=msg): + results.append(f"failed:{msg}") + case _ as unreachable: + assert_never(unreachable) + + assert results == ["accepted:req-1", "completed:['r-1']", "failed:oops"] + + +# --------------------------------------------------------------------------- +# _dispatch_single_attempt — outcome attached on success path +# --------------------------------------------------------------------------- + + +def _make_service() -> ProvisioningOrchestrationService: + container = MagicMock() + logger = MagicMock() + provider_selection_port = MagicMock() + provider_config_port = MagicMock() + config_port = MagicMock() + circuit_breaker_factory = MagicMock() + + config_port.get_request_config.return_value = { + "dispatch_timeout_seconds": 10.0, + } + + cb = MagicMock() + cb.has_state.return_value = False + circuit_breaker_factory.return_value = cb + + return ProvisioningOrchestrationService( + container=container, + logger=logger, + provider_selection_port=provider_selection_port, + provider_config_port=provider_config_port, + config_port=config_port, + circuit_breaker_factory=circuit_breaker_factory, + ) + + +def _make_request(count: int = 2): + req = MagicMock() + req.request_id = "req-outcome-test" + req.requested_count = count + req.metadata = {} + req.update_metadata = lambda d: req + return req + + +def _make_selection_result() -> ProviderSelectionResult: + return ProviderSelectionResult( + provider_name="aws_default_us-east-1", + provider_type="aws", + selection_reason="test", + confidence=1.0, + ) + + +class TestDispatchSingleAttemptOutcome: + """_dispatch_single_attempt must attach an OperationOutcome to the result.""" + + @pytest.mark.asyncio + async def test_partial_fulfillment_yields_accepted_outcome(self): + """Fewer instances than requested → Accepted (still processing).""" + from orb.providers.base.strategy.provider_strategy import ProviderResult + + svc = _make_service() + provider_result = ProviderResult.success_result( + data={ + "resource_ids": ["fleet-1"], + "instances": [{"id": "i-1"}], # only 1 of 2 requested + "instance_ids": ["i-1"], + }, + metadata={}, + ) + svc._provider_selection_port.execute_operation = AsyncMock(return_value=provider_result) + + scheduler = MagicMock() + scheduler.format_template_for_provider.return_value = {} + svc._container.get.return_value = scheduler + + result = await svc._dispatch_single_attempt( + MagicMock(template_id="t-1"), + _make_request(count=2), + _make_selection_result(), + count=2, + ) + + assert result.success is True + assert isinstance(result.outcome, Accepted) + assert result.is_final is False + + @pytest.mark.asyncio + async def test_full_fulfillment_yields_completed_outcome(self): + """All instances returned → Completed.""" + from orb.providers.base.strategy.provider_strategy import ProviderResult + + svc = _make_service() + provider_result = ProviderResult.success_result( + data={ + "resource_ids": ["fleet-1"], + "instances": [{"id": "i-1"}, {"id": "i-2"}], # 2 of 2 + "instance_ids": ["i-1", "i-2"], + }, + metadata={}, + ) + svc._provider_selection_port.execute_operation = AsyncMock(return_value=provider_result) + + scheduler = MagicMock() + scheduler.format_template_for_provider.return_value = {} + svc._container.get.return_value = scheduler + + result = await svc._dispatch_single_attempt( + MagicMock(template_id="t-1"), + _make_request(count=2), + _make_selection_result(), + count=2, + ) + + assert result.success is True + assert isinstance(result.outcome, Completed) + assert result.is_final is True + + @pytest.mark.asyncio + async def test_provider_failure_yields_failed_outcome(self): + """Provider-side error → Failed outcome attached.""" + from orb.providers.base.strategy.provider_strategy import ProviderResult + + svc = _make_service() + provider_result = ProviderResult.error_result("InsufficientCapacity", "CAPACITY_ERROR") + svc._provider_selection_port.execute_operation = AsyncMock(return_value=provider_result) + + scheduler = MagicMock() + scheduler.format_template_for_provider.return_value = {} + svc._container.get.return_value = scheduler + + result = await svc._dispatch_single_attempt( + MagicMock(template_id="t-1"), + _make_request(count=2), + _make_selection_result(), + count=2, + ) + + assert result.success is False + assert isinstance(result.outcome, Failed) + assert result.is_final is True + + @pytest.mark.asyncio + async def test_timeout_yields_recoverable_failed_outcome(self): + """Dispatch timeout → Failed(recoverable=True).""" + import asyncio + + svc = _make_service() + + async def _hang(*_a, **_kw): + await asyncio.sleep(60) + + svc._provider_selection_port.execute_operation = _hang + + scheduler = MagicMock() + scheduler.format_template_for_provider.return_value = {} + svc._container.get.return_value = scheduler + + result = await svc._dispatch_single_attempt( + MagicMock(template_id="t-1"), + _make_request(count=2), + _make_selection_result(), + count=2, + dispatch_timeout_seconds=0.05, + ) + + assert result.success is False + assert isinstance(result.outcome, Failed) + assert result.outcome.recoverable is True + assert result.is_final is True diff --git a/tests/unit/application/services/test_provisioning_telemetry_flow.py b/tests/unit/application/services/test_provisioning_telemetry_flow.py index 2f0d74de2..f20f9214d 100644 --- a/tests/unit/application/services/test_provisioning_telemetry_flow.py +++ b/tests/unit/application/services/test_provisioning_telemetry_flow.py @@ -4,6 +4,7 @@ strategy returns routing_info → app layer stamps → ProvisioningResult.provider_data has the fields """ +from typing import cast from unittest.mock import AsyncMock, MagicMock import pytest @@ -86,7 +87,7 @@ async def test_routing_info_from_aws_strategy_reaches_provider_data(): # Patch scheduler scheduler = MagicMock() scheduler.format_template_for_provider.return_value = {} - svc._container.get.return_value = scheduler + cast(MagicMock, svc._container).get.return_value = scheduler result = await svc._dispatch_single_attempt( _make_template(), _make_request(), _make_selection_result(), 1 @@ -122,7 +123,7 @@ async def test_routing_info_from_fallback_strategy_reaches_provider_data(): scheduler = MagicMock() scheduler.format_template_for_provider.return_value = {} - svc._container.get.return_value = scheduler + cast(MagicMock, svc._container).get.return_value = scheduler result = await svc._dispatch_single_attempt( _make_template(), _make_request(), _make_selection_result(), 1 @@ -162,7 +163,7 @@ async def test_routing_info_from_composite_strategy_reaches_provider_data(): scheduler = MagicMock() scheduler.format_template_for_provider.return_value = {} - svc._container.get.return_value = scheduler + cast(MagicMock, svc._container).get.return_value = scheduler result = await svc._dispatch_single_attempt( _make_template(), _make_request(), _make_selection_result(), 1 @@ -193,7 +194,7 @@ async def test_no_routing_info_does_not_break_provider_data(): scheduler = MagicMock() scheduler.format_template_for_provider.return_value = {} - svc._container.get.return_value = scheduler + cast(MagicMock, svc._container).get.return_value = scheduler result = await svc._dispatch_single_attempt( _make_template(), _make_request(), _make_selection_result(), 1 diff --git a/tests/unit/application/services/test_request_status_management_service.py b/tests/unit/application/services/test_request_status_management_service.py index 0a0f917f6..5140de960 100644 --- a/tests/unit/application/services/test_request_status_management_service.py +++ b/tests/unit/application/services/test_request_status_management_service.py @@ -98,6 +98,19 @@ def test_zero_instances_sets_in_progress(self): call_args = req.update_status.call_args[0] assert call_args[0] == RequestStatus.IN_PROGRESS + def test_zero_instances_no_resource_ids_sets_failed(self): + req = _make_request(requested_count=3) + req.resource_ids = [] + self.svc._update_request_status( + request=req, + instance_count=0, + requested_count=3, + has_api_errors=False, + provider_errors=[], + ) + call_args = req.update_status.call_args[0] + assert call_args[0] == RequestStatus.FAILED + def test_error_summary_included_in_message(self): req = _make_request(requested_count=2) errors = [{"error_code": "InsufficientCapacity", "error_message": "No capacity"}] diff --git a/tests/unit/application/services/test_request_status_service.py b/tests/unit/application/services/test_request_status_service.py index 930b719ae..9d27281f5 100644 --- a/tests/unit/application/services/test_request_status_service.py +++ b/tests/unit/application/services/test_request_status_service.py @@ -1,8 +1,16 @@ -"""Unit tests for RequestStatusService.determine_status_from_machines — return request logic.""" +"""Unit tests for RequestStatusService.determine_status_from_machines. + +The acquire path now trusts ProviderFulfilment exclusively. +The return path continues to use machine-state counting. +""" from unittest.mock import MagicMock +import pytest + from orb.application.services.request_status_service import RequestStatusService +from orb.domain.base.exceptions import ProviderContractError +from orb.domain.base.provider_fulfilment import ProviderFulfilment from orb.domain.machine.machine_status import MachineStatus from orb.domain.request.request_types import RequestStatus @@ -11,10 +19,11 @@ def _make_service(): return RequestStatusService(uow_factory=MagicMock(), logger=MagicMock()) -def _make_request(request_type="return"): +def _make_request(request_type="return", requested_count=2): req = MagicMock() req.request_type.value = request_type - req.requested_count = 2 + req.requested_count = requested_count + req.provider_name = "aws-test" return req @@ -24,6 +33,127 @@ def _make_machine(status: MachineStatus): return m +def _fulfilment(state, message="test", **kwargs) -> dict: + """Return metadata dict with a ProviderFulfilment as the acquire path expects.""" + return {"provider_fulfilment": ProviderFulfilment(state=state, message=message, **kwargs)} + + +# --------------------------------------------------------------------------- +# Acquire path — ProviderFulfilment state map +# --------------------------------------------------------------------------- + + +class TestAcquireFulfilmentStatemap: + """Acquire path: each ProviderFulfilment state maps to the right RequestStatus.""" + + def setup_method(self): + self.svc = _make_service() + + def test_fulfilled_maps_to_completed(self): + req = _make_request("acquire", requested_count=4) + machines = [_make_machine(MachineStatus.RUNNING)] * 2 + status, msg = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=req, + provider_metadata=_fulfilment( + "fulfilled", + "Fleet fulfilled", + target_units=4, + fulfilled_units=4, + running_count=2, + pending_count=0, + failed_count=0, + ), + ) + assert status == RequestStatus.COMPLETED.value + assert msg == "Fleet fulfilled" + + def test_in_progress_maps_to_in_progress(self): + req = _make_request("acquire", requested_count=4) + machines = [_make_machine(MachineStatus.PENDING)] * 4 + status, _ = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=req, + provider_metadata=_fulfilment("in_progress", "waiting"), + ) + assert status == RequestStatus.IN_PROGRESS.value + + def test_partial_maps_to_partial(self): + req = _make_request("acquire", requested_count=4) + machines = [_make_machine(MachineStatus.RUNNING)] * 2 + status, _ = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=req, + provider_metadata=_fulfilment("partial", "only 2 of 4"), + ) + assert status == RequestStatus.PARTIAL.value + + def test_failed_maps_to_failed(self): + req = _make_request("acquire", requested_count=4) + machines = [_make_machine(MachineStatus.RUNNING)] * 0 + status, _ = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=req, + provider_metadata=_fulfilment("failed", "all failed"), + ) + assert status == RequestStatus.FAILED.value + + +class TestAcquireMissingFulfilmentRaisesContractError: + """Missing ProviderFulfilment raises ProviderContractError — no silent fallback.""" + + def setup_method(self): + self.svc = _make_service() + + def test_raises_contract_error_when_fulfilment_absent(self): + req = _make_request("acquire", requested_count=2) + machines = [_make_machine(MachineStatus.RUNNING)] * 2 + with pytest.raises(ProviderContractError): + self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=req, + provider_metadata={}, # no provider_fulfilment key + ) + + def test_raises_contract_error_when_fulfilment_is_none(self): + req = _make_request("acquire", requested_count=2) + machines = [_make_machine(MachineStatus.RUNNING)] * 2 + with pytest.raises(ProviderContractError): + self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=req, + provider_metadata={"provider_fulfilment": None}, + ) + + def test_legacy_fleet_capacity_key_ignored(self): + """Old fleet_capacity_fulfilment key must NOT unlock a legacy path.""" + req = _make_request("acquire", requested_count=2) + machines = [_make_machine(MachineStatus.RUNNING)] * 2 + with pytest.raises(ProviderContractError): + self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=req, + provider_metadata={ + "fleet_capacity_fulfilment": { + "target_capacity_units": 2, + "fulfilled_capacity_units": 2.0, + } + }, + ) + + +# --------------------------------------------------------------------------- +# Return path — machine-state counting (unchanged) +# --------------------------------------------------------------------------- + + class TestReturnRequestCompletion: def setup_method(self): self.svc = _make_service() @@ -84,3 +214,136 @@ def test_return_request_with_all_stopped_is_complete(self): provider_metadata={}, ) assert status == RequestStatus.COMPLETED.value + + def test_return_request_mix_shutting_down_and_running_is_in_progress(self): + """Some shutting-down, some running → IN_PROGRESS (not complete).""" + machines = [ + _make_machine(MachineStatus.SHUTTING_DOWN), + _make_machine(MachineStatus.RUNNING), + ] + status, _ = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=self.req, + provider_metadata={}, + ) + assert status == RequestStatus.IN_PROGRESS.value + + def test_return_request_empty_provider_machines_is_complete(self): + """No instances visible in provider → all gone, COMPLETED.""" + db_machines = [_make_machine(MachineStatus.TERMINATED)] + status, msg = self.svc.determine_status_from_machines( + db_machines=db_machines, # type: ignore[arg-type] + provider_machines=[], + request=self.req, + provider_metadata={}, + ) + assert status == RequestStatus.COMPLETED.value + assert "no longer visible" in (msg or "") + + +class TestPrematureCompletedRegression: + """Regression guard: COMPLETED must NOT be written when termination is merely accepted.""" + + def setup_method(self): + self.svc = _make_service() + self.req = _make_request("return") + + def test_shutting_down_instance_yields_in_progress_not_completed(self): + machines = [_make_machine(MachineStatus.SHUTTING_DOWN)] + status, _ = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=self.req, + provider_metadata={}, + ) + assert status != RequestStatus.COMPLETED.value + assert status == RequestStatus.IN_PROGRESS.value + + def test_mix_shutting_down_terminated_yields_in_progress(self): + machines = [ + _make_machine(MachineStatus.SHUTTING_DOWN), + _make_machine(MachineStatus.TERMINATED), + _make_machine(MachineStatus.SHUTTING_DOWN), + ] + status, _ = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=self.req, + provider_metadata={}, + ) + assert status == RequestStatus.IN_PROGRESS.value + + def test_all_terminated_yields_completed(self): + machines = [ + _make_machine(MachineStatus.TERMINATED), + _make_machine(MachineStatus.TERMINATED), + _make_machine(MachineStatus.TERMINATED), + ] + status, _ = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=self.req, + provider_metadata={}, + ) + assert status == RequestStatus.COMPLETED.value + + +class TestReturnPartialDescribeGuard: + """Regression guard: COMPLETED must NOT fire when describe returns fewer machines.""" + + def setup_method(self): + self.svc = _make_service() + + def test_partial_describe_terminated_not_complete(self): + """3 terminated visible, requested_count=4 → IN_PROGRESS (1 not yet in response).""" + req = _make_request("return", requested_count=4) + machines = [ + _make_machine(MachineStatus.TERMINATED), + _make_machine(MachineStatus.TERMINATED), + _make_machine(MachineStatus.TERMINATED), + ] + status, _ = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=req, + provider_metadata={}, + ) + assert status != RequestStatus.COMPLETED.value + assert status == RequestStatus.IN_PROGRESS.value + + def test_all_requested_terminated_is_completed(self): + req = _make_request("return", requested_count=4) + machines = [_make_machine(MachineStatus.TERMINATED)] * 4 + status, _ = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=req, + provider_metadata={}, + ) + assert status == RequestStatus.COMPLETED.value + + def test_more_terminated_than_requested_is_completed(self): + req = _make_request("return", requested_count=2) + machines = [_make_machine(MachineStatus.TERMINATED)] * 3 + status, _ = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=req, + provider_metadata={}, + ) + assert status == RequestStatus.COMPLETED.value + + def test_one_terminated_one_shutting_down_not_complete(self): + req = _make_request("return", requested_count=2) + machines = [ + _make_machine(MachineStatus.TERMINATED), + _make_machine(MachineStatus.SHUTTING_DOWN), + ] + status, _ = self.svc.determine_status_from_machines( + db_machines=machines, # type: ignore[arg-type] + provider_machines=machines, # type: ignore[arg-type] + request=req, + provider_metadata={}, + ) + assert status == RequestStatus.IN_PROGRESS.value diff --git a/tests/unit/application/services/test_template_defaults_service.py b/tests/unit/application/services/test_template_defaults_service.py new file mode 100644 index 000000000..e3db69277 --- /dev/null +++ b/tests/unit/application/services/test_template_defaults_service.py @@ -0,0 +1,153 @@ +"""Tests for TemplateDefaultsService — registry delegation and default-API chain.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +from orb.application.services.template_defaults_service import TemplateDefaultsService + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_service( + provider_defaults: dict | None = None, + global_config: dict | None = None, + provider_registry: object | None = None, +) -> TemplateDefaultsService: + """Build a TemplateDefaultsService with controllable config mocks.""" + logger = MagicMock() + config_manager = MagicMock() + + # Mimic get_template_config() returning a dict (no model_dump needed). + config_manager.get_template_config.return_value = global_config or {} + + # Mimic get_provider_config().provider_defaults.get(provider_type). + if provider_defaults is not None: + mock_provider_config = MagicMock() + mock_provider_config.provider_defaults = provider_defaults + mock_provider_config.providers = [] + else: + mock_provider_config = MagicMock(provider_defaults={}, providers=[]) + config_manager.get_provider_config.return_value = mock_provider_config + + return TemplateDefaultsService( + config_manager=config_manager, + logger=logger, + provider_registry=provider_registry, # type: ignore[arg-type] + ) + + +def _make_provider_defaults_with_api(provider_api: str | None) -> MagicMock: + """Create a provider_defaults mock with template_defaults.provider_api set.""" + defaults_obj = MagicMock() + template_defaults: dict = {} + if provider_api is not None: + template_defaults["provider_api"] = provider_api + defaults_obj.template_defaults = template_defaults + return defaults_obj + + +# --------------------------------------------------------------------------- +# _get_provider_type_defaults — registry delegation +# --------------------------------------------------------------------------- + + +class TestGetProviderTypeDefaultsDelegatesRegistry: + """When template_defaults has no provider_api, registry is consulted.""" + + def test_registry_default_api_fills_missing_provider_api(self): + """Registry.get_default_api() result is injected when template_defaults lacks provider_api.""" + registry = MagicMock() + registry.get_default_api.return_value = "EC2Fleet" + + provider_defaults_obj = _make_provider_defaults_with_api(None) + svc = _make_service( + provider_defaults={"aws": provider_defaults_obj}, + provider_registry=registry, + ) + + result = svc._get_provider_type_defaults("aws") + + assert result.get("provider_api") == "EC2Fleet" + registry.get_default_api.assert_called_once_with("aws") + + def test_registry_not_called_when_template_defaults_has_provider_api(self): + """Registry is NOT consulted when template_defaults already has provider_api.""" + registry = MagicMock() + registry.get_default_api.return_value = "SpotFleet" + + provider_defaults_obj = _make_provider_defaults_with_api("EC2Fleet") + svc = _make_service( + provider_defaults={"aws": provider_defaults_obj}, + provider_registry=registry, + ) + + result = svc._get_provider_type_defaults("aws") + + assert result.get("provider_api") == "EC2Fleet" + registry.get_default_api.assert_not_called() + + def test_no_registry_injected_returns_empty_provider_api(self): + """Without registry, if template_defaults has no provider_api, result has none.""" + provider_defaults_obj = _make_provider_defaults_with_api(None) + svc = _make_service( + provider_defaults={"aws": provider_defaults_obj}, + provider_registry=None, + ) + + result = svc._get_provider_type_defaults("aws") + + assert "provider_api" not in result + + def test_registry_returns_none_leaves_provider_api_absent(self): + """When registry returns None, provider_api is not added to result.""" + registry = MagicMock() + registry.get_default_api.return_value = None + + provider_defaults_obj = _make_provider_defaults_with_api(None) + svc = _make_service( + provider_defaults={"aws": provider_defaults_obj}, + provider_registry=registry, + ) + + result = svc._get_provider_type_defaults("aws") + + assert "provider_api" not in result + + def test_unknown_provider_type_returns_empty_dict(self): + """A provider type with no registration returns {}.""" + registry = MagicMock() + registry.get_default_api.return_value = None + svc = _make_service(provider_defaults={}, provider_registry=registry) + + result = svc._get_provider_type_defaults("unknown-provider") + + assert result == {} + + +# --------------------------------------------------------------------------- +# TemplateDefaultsService init — provider_registry optional +# --------------------------------------------------------------------------- + + +class TestTemplateDefaultsServiceInit: + def test_no_registry_param_accepted(self): + """Service can be instantiated without provider_registry (backward compat).""" + logger = MagicMock() + config_manager = MagicMock() + config_manager.get_template_config.return_value = {} + config_manager.get_provider_config.return_value = MagicMock( + provider_defaults={}, providers=[] + ) + + svc = TemplateDefaultsService(config_manager=config_manager, logger=logger) + + assert svc.provider_registry is None + + def test_registry_stored_on_instance(self): + """Injected provider_registry is accessible via attribute.""" + registry = MagicMock() + svc = _make_service(provider_registry=registry) + assert svc.provider_registry is registry diff --git a/tests/unit/application/test_cqrs_patterns.py b/tests/unit/application/test_cqrs_patterns.py index d42505a97..ef3ac51e8 100644 --- a/tests/unit/application/test_cqrs_patterns.py +++ b/tests/unit/application/test_cqrs_patterns.py @@ -168,6 +168,15 @@ async def test_command_handlers_modify_state(self): mock_provisioning_service = Mock() + mock_selection_result = Mock() + mock_selection_result.provider_type = "aws" + mock_selection_result.provider_name = "test-provider" + mock_selection_result.selection_reason = "test" + mock_provider_validation_service = AsyncMock() + mock_provider_validation_service.select_and_validate_provider.return_value = ( + mock_selection_result + ) + handler = CreateRequestHandler( uow_factory=mock_uow_factory, logger=mock_logger, @@ -177,6 +186,7 @@ async def test_command_handlers_modify_state(self): query_bus=mock_query_bus, provider_selection_port=mock_provider_selection, provisioning_service=mock_provisioning_service, + provider_validation_service=mock_provider_validation_service, ) # Execute command with dry_run — PENDING→COMPLETED is now valid @@ -470,6 +480,17 @@ async def test_create_request_handler_validates_input(self): mock_provider_port = Mock() mock_provider_port.available_strategies = ["test-strategy"] + mock_selection_result = Mock() + mock_selection_result.provider_instance = "test-provider" + mock_selection_result.provider_type = "aws" + mock_selection_result.provider_name = "test-provider" + mock_selection_result.selection_reason = "test" + mock_selection_result.confidence = 1.0 + mock_provider_validation_service = AsyncMock() + mock_provider_validation_service.select_and_validate_provider.return_value = ( + mock_selection_result + ) + handler = CreateRequestHandler( uow_factory=mock_uow_factory, logger=mock_logger, @@ -479,6 +500,7 @@ async def test_create_request_handler_validates_input(self): query_bus=mock_query_bus, provider_selection_port=mock_provider_selection, provisioning_service=Mock(), + provider_validation_service=mock_provider_validation_service, ) # Valid command @@ -493,20 +515,6 @@ async def test_create_request_handler_validates_input(self): mock_template.to_dict.return_value = {"template_id": "test-template"} mock_query_bus.execute.return_value = mock_template - mock_selection_result = Mock() - mock_selection_result.provider_instance = "test-provider" - mock_selection_result.provider_type = "aws" - mock_selection_result.provider_name = "test-provider" - mock_selection_result.selection_reason = "test" - mock_selection_result.confidence = 1.0 - mock_provider_selection.select_provider_for_template.return_value = mock_selection_result - mock_provider_validation_result = Mock() - mock_provider_validation_result.is_valid = True - mock_provider_validation_result.warnings = [] - mock_provider_selection.validate_template_requirements.return_value = ( - mock_provider_validation_result - ) - mock_provider_capability = Mock() mock_validation_result = Mock() mock_validation_result.is_valid = True @@ -564,21 +572,9 @@ async def test_command_handlers_are_transactional(self): mock_selection_result.provider_name = "test-provider" mock_selection_result.selection_reason = "test" mock_selection_result.confidence = 1.0 - mock_provider_selection.select_provider_for_template.return_value = mock_selection_result - mock_provider_validation_result = Mock() - mock_provider_validation_result.is_valid = True - mock_provider_validation_result.warnings = [] - mock_provider_selection.validate_template_requirements.return_value = ( - mock_provider_validation_result - ) - - mock_provider_capability = Mock() - mock_validation_result = Mock() - mock_validation_result.is_valid = True - mock_validation_result.supported_features = [] - mock_validation_result.warnings = [] - mock_provider_capability.validate_template_requirements.return_value = ( - mock_validation_result + mock_provider_validation_service = AsyncMock() + mock_provider_validation_service.select_and_validate_provider.return_value = ( + mock_selection_result ) mock_provider_port = Mock() @@ -593,6 +589,7 @@ async def test_command_handlers_are_transactional(self): query_bus=mock_query_bus, provider_selection_port=mock_provider_selection, provisioning_service=Mock(), + provider_validation_service=mock_provider_validation_service, ) # Execute command with dry_run — PENDING→COMPLETED is now valid @@ -634,6 +631,7 @@ def test_command_handlers_publish_events(self): query_bus=mock_query_bus, provider_selection_port=mock_provider_selection, provisioning_service=Mock(), + provider_validation_service=Mock(), ) # Should have event publisher diff --git a/tests/unit/architecture/test_boto3_leak_detection.py b/tests/unit/architecture/test_boto3_leak_detection.py new file mode 100644 index 000000000..d6ae7b171 --- /dev/null +++ b/tests/unit/architecture/test_boto3_leak_detection.py @@ -0,0 +1,59 @@ +"""Architecture test: boto3 and botocore must not be imported outside providers/aws/. + +Importing boto3 or botocore directly in core or infrastructure modules couples the +entire application to the [aws] extra even when it is not installed. All boto3/botocore +usage must be confined to src/orb/providers/aws/. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from tests.unit.architecture.conftest import ( + EXCEPTION_PATHS, + SRC_ORB, + collect_python_files, + extract_imports, +) + +_PROVIDERS_AWS_DIR = SRC_ORB / "providers" / "aws" + +# All source files that live outside providers/aws/ +_NON_AWS_FILES = [ + f + for f in collect_python_files(SRC_ORB) + if not f.is_relative_to(_PROVIDERS_AWS_DIR) and str(f) not in EXCEPTION_PATHS +] + +# Top-level module names that constitute direct AWS SDK imports +_AWS_SDK_MODULES = frozenset({"boto3", "botocore"}) + +# Known violations — files currently allowed to import boto3/botocore outside providers/aws/. +# This set should remain empty; add entries only as a last resort with a tracking comment. +_KNOWN_VIOLATIONS: frozenset[tuple[str, str]] = frozenset() + + +@pytest.mark.parametrize( + "filepath", + _NON_AWS_FILES, + ids=lambda p: str(p.relative_to(SRC_ORB)), +) +@pytest.mark.unit +@pytest.mark.architecture +def test_no_boto3_outside_aws_provider(filepath: Path) -> None: + """boto3/botocore imports must not appear outside src/orb/providers/aws/.""" + rel = str(filepath.relative_to(SRC_ORB)) + imports = extract_imports(filepath) + new_violations = [ + imp + for imp in imports + if (imp in _AWS_SDK_MODULES or any(imp.startswith(f"{m}.") for m in _AWS_SDK_MODULES)) + and (rel, imp) not in _KNOWN_VIOLATIONS + ] + assert new_violations == [], ( + f"{rel} imports boto3/botocore outside providers/aws/ — " + f"move to providers/aws/ or guard with try/except ImportError. " + f"Violations: {new_violations}" + ) diff --git a/tests/unit/architecture/test_cli_infrastructure_boundary.py b/tests/unit/architecture/test_cli_infrastructure_boundary.py index d227be818..408229eb4 100644 --- a/tests/unit/architecture/test_cli_infrastructure_boundary.py +++ b/tests/unit/architecture/test_cli_infrastructure_boundary.py @@ -34,6 +34,7 @@ ("cli/main.py", "orb.infrastructure.mocking.dry_run_context"), ("cli/main.py", "orb.infrastructure.di.container"), ("cli/factories/provider_command_factory.py", "orb.infrastructure.utilities.json_utils"), + ("cli/args.py", "orb.infrastructure.registry.cli_spec_registry"), } ) diff --git a/tests/unit/architecture/test_domain_purity.py b/tests/unit/architecture/test_domain_purity.py index dfebe7261..aa587a3a1 100644 --- a/tests/unit/architecture/test_domain_purity.py +++ b/tests/unit/architecture/test_domain_purity.py @@ -27,7 +27,6 @@ _KNOWN_PYDANTIC_VIOLATIONS: frozenset[tuple[str, str]] = frozenset( { ("domain/template/template_aggregate.py", "pydantic"), - ("domain/template/extensions.py", "pydantic"), ("domain/machine/aggregate.py", "pydantic"), ("domain/machine/machine_metadata.py", "pydantic"), ("domain/machine/machine_identifiers.py", "pydantic"), diff --git a/tests/unit/architecture/test_interface_provider_boundary.py b/tests/unit/architecture/test_interface_provider_boundary.py index c6352887c..cf9799285 100644 --- a/tests/unit/architecture/test_interface_provider_boundary.py +++ b/tests/unit/architecture/test_interface_provider_boundary.py @@ -41,8 +41,9 @@ ("interface/init_command_handler.py", "orb.providers.factory"), ("interface/machine_command_handlers.py", "orb.providers.base.strategy"), ("interface/mcp/server/core.py", "orb.providers.registry"), - ("api/server.py", "orb.providers.aws.auth.iam_strategy"), - ("api/server.py", "orb.providers.aws.auth.cognito_strategy"), + # CLI spec bootstrap: build_parser triggers lightweight CLI-spec registration + # so that provider flags (e.g. --aws-profile) are available before app init. + ("cli/args.py", "orb.providers.registration"), } ) diff --git a/tests/unit/architecture/test_provider_leak_detection.py b/tests/unit/architecture/test_provider_leak_detection.py index 048c2e058..73035a819 100644 --- a/tests/unit/architecture/test_provider_leak_detection.py +++ b/tests/unit/architecture/test_provider_leak_detection.py @@ -35,9 +35,9 @@ _KNOWN_VIOLATIONS: frozenset[tuple[str, str]] = frozenset( { ("bootstrap/core_services.py", "orb.providers.registry"), - ("bootstrap/infrastructure_services.py", "orb.providers.aws.registration"), + ("bootstrap/infrastructure_services.py", "orb.providers.registration"), ("bootstrap/provider_services.py", "orb.providers.registry"), - ("bootstrap/provider_services.py", "orb.providers.aws.registration"), + ("bootstrap/provider_services.py", "orb.providers.registration"), ("bootstrap/services.py", "orb.providers.registration"), ("interface/health_command_handler.py", "orb.providers.registry"), ("interface/system_command_handlers.py", "orb.providers.registry"), @@ -48,8 +48,6 @@ ("interface/init_command_handler.py", "orb.providers.factory"), ("interface/machine_command_handlers.py", "orb.providers.base.strategy"), ("interface/mcp/server/core.py", "orb.providers.registry"), - ("api/server.py", "orb.providers.aws.auth.iam_strategy"), - ("api/server.py", "orb.providers.aws.auth.cognito_strategy"), ("config/managers/provider_manager.py", "orb.providers.registry"), ("application/services/provider_registry_service.py", "orb.providers.registry"), ("infrastructure/template/configuration_manager.py", "orb.providers.base.strategy"), @@ -57,18 +55,17 @@ ("infrastructure/adapters/provider_discovery_adapter.py", "orb.providers.registry"), # DI wiring — intentional: storage registration must wire AWS provider ("infrastructure/storage/registration.py", "orb.providers.aws.storage.registration"), - # hostfactory is inherently AWS/HPC-specific — provider import is expected - ( - "infrastructure/scheduler/hostfactory/field_mapper.py", - "orb.providers.aws.utilities.ec2.instances", - ), - ( - "infrastructure/scheduler/hostfactory/hostfactory_strategy.py", - "orb.providers.aws.utilities.ec2.instances", - ), + # The cpu/ram lookup (derive_cpu_ram_from_instance_type) has been moved + # into providers/aws/scheduler/hostfactory_field_mapping.py. The shared + # hostfactory infrastructure no longer imports from orb.providers.aws.*. ("config/schemas/cleanup_schema.py", "orb.providers.aws.configuration.cleanup_config"), # loader collects strategy-contributed defaults at load time — intentional bootstrap wiring ("config/loader.py", "orb.providers.registry"), + ("config/loader.py", "orb.providers"), + ("config/loader.py", "orb.providers.registration"), + # CLI spec bootstrap: build_parser triggers lightweight CLI-spec registration + # so that provider flags (e.g. --aws-profile) are available before app init. + ("cli/args.py", "orb.providers.registration"), } ) diff --git a/tests/unit/architecture/violation_counts.json b/tests/unit/architecture/violation_counts.json index 139d1834a..a5f14bc5a 100644 --- a/tests/unit/architecture/violation_counts.json +++ b/tests/unit/architecture/violation_counts.json @@ -4,8 +4,8 @@ "provider_leaks_non_di": 29, "scheduler_leaks": 0, "storage_leaks_non_storage": 4, - "domain_pydantic_violations": 13, + "domain_pydantic_violations": 12, "interface_provider_direct_imports": 16, "circular_dependency_cycles": 18, - "cli_infrastructure_direct_imports": 13 + "cli_infrastructure_direct_imports": 14 } diff --git a/tests/unit/config/test_server_schema.py b/tests/unit/config/test_server_schema.py new file mode 100644 index 000000000..e0b552a6f --- /dev/null +++ b/tests/unit/config/test_server_schema.py @@ -0,0 +1,52 @@ +"""Unit tests for server_schema configuration models.""" + +from __future__ import annotations + +import pytest +from pydantic import ValidationError + +# --------------------------------------------------------------------------- +# BearerTokenAuthSubConfig — algorithm allowlist validator +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +def test_bearer_token_config_default_algorithm_is_hs256(): + """Default algorithm is HS256.""" + from orb.config.schemas.server_schema import BearerTokenAuthSubConfig + + cfg = BearerTokenAuthSubConfig(secret_key="a" * 32) # type: ignore[call-arg] # pydantic default fields + assert cfg.algorithm == "HS256" + + +@pytest.mark.unit +@pytest.mark.parametrize("algo", ["HS256", "HS384", "HS512"]) +def test_bearer_token_config_valid_algorithms(algo: str): + """HS256, HS384, and HS512 are all accepted.""" + from orb.config.schemas.server_schema import BearerTokenAuthSubConfig + + cfg = BearerTokenAuthSubConfig(secret_key="a" * 32, algorithm=algo) # type: ignore[call-arg] # pydantic default fields + assert cfg.algorithm == algo + + +@pytest.mark.unit +@pytest.mark.parametrize("algo", ["none", "None", "NONE"]) +def test_bearer_token_config_rejects_none_algorithm(algo: str): + """algorithm='none' (any casing) raises ValidationError with a descriptive message.""" + from orb.config.schemas.server_schema import BearerTokenAuthSubConfig + + with pytest.raises(ValidationError) as exc_info: + BearerTokenAuthSubConfig(secret_key="a" * 32, algorithm=algo) # type: ignore[call-arg] # pydantic default fields + + errors = exc_info.value.errors() + assert any("none" in str(e).lower() for e in errors) + + +@pytest.mark.unit +@pytest.mark.parametrize("algo", ["RS256", "ES256", "HS1", "unknown"]) +def test_bearer_token_config_rejects_unsupported_algorithms(algo: str): + """Non-HMAC and unknown algorithms raise ValidationError.""" + from orb.config.schemas.server_schema import BearerTokenAuthSubConfig + + with pytest.raises(ValidationError): + BearerTokenAuthSubConfig(secret_key="a" * 32, algorithm=algo) # type: ignore[call-arg] # pydantic default fields diff --git a/tests/unit/infrastructure/auth/test_enhanced_bearer_token.py b/tests/unit/infrastructure/auth/test_enhanced_bearer_token.py new file mode 100644 index 000000000..ea94558e3 --- /dev/null +++ b/tests/unit/infrastructure/auth/test_enhanced_bearer_token.py @@ -0,0 +1,242 @@ +"""Unit tests for EnhancedBearerTokenStrategy.""" + +from __future__ import annotations + +import time +from typing import Any + +import jwt +import pytest + +from orb.infrastructure.adapters.ports.auth import AuthContext, AuthStatus +from orb.infrastructure.auth.strategy.bearer_token_strategy_enhanced import ( + EnhancedBearerTokenStrategy, +) +from orb.infrastructure.auth.token_denylist.in_memory_denylist import InMemoryTokenDenylist + +_SECRET = "a" * 32 # exactly 32 bytes — minimum valid length + + +def _make_denylist() -> InMemoryTokenDenylist: + return InMemoryTokenDenylist() + + +def _make_strategy( + denylist: InMemoryTokenDenylist | None = None, + rate_limit_enabled: bool = False, +) -> EnhancedBearerTokenStrategy: + return EnhancedBearerTokenStrategy( + secret_key=_SECRET, + denylist=denylist or _make_denylist(), + rate_limit_enabled=rate_limit_enabled, + ) + + +def _make_context(**kwargs) -> AuthContext: + defaults: dict[str, Any] = { + "method": "GET", + "path": "/api/v1/machines", + "headers": {}, + "query_params": {}, + "client_ip": "127.0.0.1", + } + defaults.update(kwargs) + return AuthContext(**defaults) + + +def _make_token( + user_id: str = "user-1", + roles: list[str] | None = None, + expiry_offset: int = 3600, +) -> str: + now = int(time.time()) + payload = { + "sub": user_id, + "roles": roles or ["user"], + "permissions": [], + "type": "access", + "iat": now, + "exp": now + expiry_offset, + "iss": "open-resource-broker", + } + return jwt.encode(payload, _SECRET, algorithm="HS256") + + +# --------------------------------------------------------------------------- +# authenticate() — happy path +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_enhanced_bearer_authenticate_success(): + """authenticate() returns SUCCESS for a valid Bearer token.""" + strategy = _make_strategy() + token = _make_token() + + result = await strategy.authenticate( + _make_context(headers={"authorization": f"Bearer {token}"}) + ) + + assert result.status == AuthStatus.SUCCESS + assert result.user_id == "user-1" + + +# --------------------------------------------------------------------------- +# authenticate() — missing header +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_enhanced_bearer_authenticate_missing_header(): + """authenticate() returns FAILED when Authorization header is absent.""" + strategy = _make_strategy() + + result = await strategy.authenticate(_make_context(headers={})) + + assert result.status == AuthStatus.FAILED + + +# --------------------------------------------------------------------------- +# authenticate() — denylisted token +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_enhanced_bearer_authenticate_denylisted_token(): + """validate_token() returns INVALID for a denylisted token.""" + denylist = _make_denylist() + strategy = _make_strategy(denylist=denylist) + token = _make_token() + + await denylist.add_token(token) + result = await strategy.validate_token(token) + + assert result.status == AuthStatus.INVALID + assert "revoked" in (result.error_message or "").lower() + + +# --------------------------------------------------------------------------- +# authenticate() — rate limit +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_enhanced_bearer_rate_limit(): + """authenticate() returns FAILED when rate limit is exceeded.""" + strategy = EnhancedBearerTokenStrategy( + secret_key=_SECRET, + denylist=_make_denylist(), + rate_limit_enabled=True, + max_attempts=1, + rate_window=60, + ) + + ctx = _make_context(headers={"authorization": "Bearer invalid"}) + + # First attempt consumes the single allowed slot (will fail on bad token, not rate limit) + await strategy.authenticate(ctx) + + # Second attempt should hit the rate limit + result = await strategy.authenticate(ctx) + assert result.status == AuthStatus.FAILED + assert "too many" in (result.error_message or "").lower() + + +# --------------------------------------------------------------------------- +# revoke_token() +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_enhanced_bearer_revoke_token(): + """revoke_token() adds the token to the denylist.""" + denylist = _make_denylist() + strategy = _make_strategy(denylist=denylist) + token = _make_token() + + success = await strategy.revoke_token(token) + assert success is True + assert await denylist.is_denylisted(token) is True + + +# --------------------------------------------------------------------------- +# validate_token() — invalid signature +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.unit +async def test_enhanced_bearer_invalid_signature(): + """validate_token() returns INVALID for a token signed with a different key.""" + strategy = _make_strategy() + wrong_token = jwt.encode( + {"sub": "x", "iat": int(time.time()), "exp": int(time.time()) + 60}, + "b" * 32, + algorithm="HS256", + ) + + result = await strategy.validate_token(wrong_token) + + assert result.status == AuthStatus.INVALID + + +# --------------------------------------------------------------------------- +# from_auth_config classmethod +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +def test_enhanced_bearer_from_auth_config(): + """from_auth_config builds strategy from typed BearerTokenAuthSubConfig.""" + from orb.config.schemas.server_schema import AuthConfig, BearerTokenAuthSubConfig + + auth_config = AuthConfig( # type: ignore[call-arg] # pydantic default fields + strategy="bearer_token_enhanced", + bearer_token=BearerTokenAuthSubConfig( # type: ignore[call-arg] # pydantic default fields + secret_key=_SECRET + ), + ) + + strategy = EnhancedBearerTokenStrategy.from_auth_config(auth_config) + + assert strategy.secret_key == _SECRET + assert strategy.enabled is True + + +@pytest.mark.unit +def test_enhanced_bearer_from_auth_config_missing_bearer_token(): + """from_auth_config raises ConfigurationError when bearer_token sub-config is absent.""" + from orb.config.schemas.server_schema import AuthConfig + from orb.domain.base.exceptions import ConfigurationError + + auth_config = AuthConfig(strategy="bearer_token_enhanced") # type: ignore[call-arg] # pydantic default fields + + with pytest.raises(ConfigurationError): + EnhancedBearerTokenStrategy.from_auth_config(auth_config) + + +# --------------------------------------------------------------------------- +# Short secret key → ConfigurationError (not a warning) +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +def test_enhanced_bearer_short_key_raises_configuration_error(): + """ + EnhancedBearerTokenStrategy must raise ConfigurationError for keys shorter + than 32 bytes, matching the error policy of BearerTokenStrategy. + """ + from orb.domain.base.exceptions import ConfigurationError + + short_key = "a" * 16 # 16 bytes — below the 32-byte minimum + + with pytest.raises(ConfigurationError, match="32 bytes"): + EnhancedBearerTokenStrategy( + secret_key=short_key, + denylist=_make_denylist(), + ) diff --git a/tests/unit/infrastructure/auth/test_token_blacklist.py b/tests/unit/infrastructure/auth/test_token_blacklist.py deleted file mode 100644 index 58e9619de..000000000 --- a/tests/unit/infrastructure/auth/test_token_blacklist.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Tests for token blacklist implementations.""" - -import time - -import pytest - -from orb.infrastructure.auth.token_blacklist import InMemoryTokenBlacklist - - -@pytest.mark.asyncio -async def test_in_memory_blacklist_add_token(): - """Test adding token to blacklist.""" - blacklist = InMemoryTokenBlacklist() - - token = "test_token_123" - expires_at = int(time.time()) + 3600 - - result = await blacklist.add_token(token, expires_at) - assert result is True - - is_blacklisted = await blacklist.is_blacklisted(token) - assert is_blacklisted is True - - -@pytest.mark.asyncio -async def test_in_memory_blacklist_remove_token(): - """Test removing token from blacklist.""" - blacklist = InMemoryTokenBlacklist() - - token = "test_token_123" - await blacklist.add_token(token) - - result = await blacklist.remove_token(token) - assert result is True - - is_blacklisted = await blacklist.is_blacklisted(token) - assert is_blacklisted is False - - -@pytest.mark.asyncio -async def test_in_memory_blacklist_expired_token(): - """Test that expired tokens are automatically removed.""" - blacklist = InMemoryTokenBlacklist() - - token = "test_token_123" - expires_at = int(time.time()) - 1 # Already expired - - await blacklist.add_token(token, expires_at) - - # Token should be removed when checked - is_blacklisted = await blacklist.is_blacklisted(token) - assert is_blacklisted is False - - -@pytest.mark.asyncio -async def test_in_memory_blacklist_cleanup(): - """Test cleanup of expired tokens.""" - blacklist = InMemoryTokenBlacklist() - - # Add expired token - expired_token = "expired_token" - await blacklist.add_token(expired_token, int(time.time()) - 1) - - # Add valid token - valid_token = "valid_token" - await blacklist.add_token(valid_token, int(time.time()) + 3600) - - # Run cleanup - removed = await blacklist.cleanup_expired() - assert removed == 1 - - # Valid token should still be there - assert await blacklist.is_blacklisted(valid_token) is True - - -@pytest.mark.asyncio -async def test_in_memory_blacklist_size(): - """Test getting blacklist size.""" - blacklist = InMemoryTokenBlacklist() - - assert await blacklist.get_blacklist_size() == 0 - - await blacklist.add_token("token1") - await blacklist.add_token("token2") - - assert await blacklist.get_blacklist_size() == 2 diff --git a/tests/unit/infrastructure/auth/test_token_denylist.py b/tests/unit/infrastructure/auth/test_token_denylist.py new file mode 100644 index 000000000..a11b0d2e6 --- /dev/null +++ b/tests/unit/infrastructure/auth/test_token_denylist.py @@ -0,0 +1,86 @@ +"""Tests for token denylist implementations.""" + +import time + +import pytest + +from orb.infrastructure.auth.token_denylist import InMemoryTokenDenylist + + +@pytest.mark.asyncio +async def test_in_memory_denylist_add_token(): + """Test adding token to denylist.""" + denylist = InMemoryTokenDenylist() + + token = "test_token_123" + expires_at = int(time.time()) + 3600 + + result = await denylist.add_token(token, expires_at) + assert result is True + + is_denylisted = await denylist.is_denylisted(token) + assert is_denylisted is True + + +@pytest.mark.asyncio +async def test_in_memory_denylist_remove_token(): + """Test removing token from denylist.""" + denylist = InMemoryTokenDenylist() + + token = "test_token_123" + await denylist.add_token(token) + + result = await denylist.remove_token(token) + assert result is True + + is_denylisted = await denylist.is_denylisted(token) + assert is_denylisted is False + + +@pytest.mark.asyncio +async def test_in_memory_denylist_expired_token(): + """Test that expired tokens are automatically removed.""" + denylist = InMemoryTokenDenylist() + + token = "test_token_123" + expires_at = int(time.time()) - 1 # Already expired + + await denylist.add_token(token, expires_at) + + # Token should be removed when checked + is_denylisted = await denylist.is_denylisted(token) + assert is_denylisted is False + + +@pytest.mark.asyncio +async def test_in_memory_denylist_cleanup(): + """Test cleanup of expired tokens.""" + denylist = InMemoryTokenDenylist() + + # Add expired token + expired_token = "expired_token" + await denylist.add_token(expired_token, int(time.time()) - 1) + + # Add valid token + valid_token = "valid_token" + await denylist.add_token(valid_token, int(time.time()) + 3600) + + # Run cleanup + removed = await denylist.cleanup_expired() + assert removed == 1 + + # Valid token should still be there + assert await denylist.is_denylisted(valid_token) is True + + +@pytest.mark.asyncio +async def test_in_memory_denylist_size(): + """Test getting denylist size.""" + denylist = InMemoryTokenDenylist() + + assert await denylist.get_denylist_size() == 0 + + await denylist.add_token("token1") + await denylist.add_token("token2") + + assert await denylist.get_denylist_size() == 2 diff --git a/tests/unit/infrastructure/scheduler/conftest.py b/tests/unit/infrastructure/scheduler/conftest.py index f8858e59d..bd09157e1 100644 --- a/tests/unit/infrastructure/scheduler/conftest.py +++ b/tests/unit/infrastructure/scheduler/conftest.py @@ -14,7 +14,7 @@ from orb.infrastructure.scheduler.hostfactory.hostfactory_strategy import ( HostFactorySchedulerStrategy, ) -from tests.onaws.plugin_io_schemas import ( +from tests.providers.aws.live.plugin_io_schemas import ( expected_get_available_templates_schema_default, expected_get_available_templates_schema_hostfactory, expected_request_machines_schema_default, diff --git a/tests/unit/infrastructure/scheduler/test_response_formatting.py b/tests/unit/infrastructure/scheduler/test_response_formatting.py index 78cbb9c5e..c0d327441 100644 --- a/tests/unit/infrastructure/scheduler/test_response_formatting.py +++ b/tests/unit/infrastructure/scheduler/test_response_formatting.py @@ -25,7 +25,7 @@ HostFactorySchedulerStrategy, ) from orb.infrastructure.template.dtos import TemplateDTO -from tests.onaws.plugin_io_schemas import ( +from tests.providers.aws.live.plugin_io_schemas import ( expected_get_available_templates_schema_default, expected_get_available_templates_schema_hostfactory, expected_request_status_schema_default, diff --git a/tests/unit/infrastructure/template/test_template_dto_no_aws_fields.py b/tests/unit/infrastructure/template/test_template_dto_no_aws_fields.py index 05c78c563..c4ee61271 100644 --- a/tests/unit/infrastructure/template/test_template_dto_no_aws_fields.py +++ b/tests/unit/infrastructure/template/test_template_dto_no_aws_fields.py @@ -10,6 +10,7 @@ AWSRequiredIntegerRange, AWSTemplate, ) +from orb.providers.aws.domain.template.aws_template_dto_config import AWSTemplateDTOConfig # --------------------------------------------------------------------------- # AST scan — no top-level AWS fields on TemplateDTO @@ -19,7 +20,13 @@ os.path.dirname(__file__), "../../../../src/orb/infrastructure/template/dtos.py", ) -_AWS_FIELDS = {"fleet_role", "fleet_type", "percent_on_demand", "abis_instance_requirements"} +_AWS_FIELDS = { + "fleet_role", + "fleet_type", + "percent_on_demand", + "abis_instance_requirements", + "launch_template_id", +} def _get_template_dto_field_names() -> set[str]: @@ -65,14 +72,27 @@ def test_abis_instance_requirements_not_a_top_level_field(self): "abis_instance_requirements is an AWS-specific field and must not be a top-level TemplateDTO attribute" ) + def test_launch_template_id_not_a_top_level_field(self): + fields = _get_template_dto_field_names() + assert "launch_template_id" not in fields, ( + "launch_template_id is an AWS-specific field and must not be a top-level TemplateDTO attribute" + ) + def test_no_aws_fields_at_all(self): fields = _get_template_dto_field_names() present = _AWS_FIELDS & fields assert not present, f"AWS-specific fields still declared on TemplateDTO: {present}" + def test_provider_config_field_present(self): + """TemplateDTO must expose a typed provider_config field.""" + fields = _get_template_dto_field_names() + assert "provider_config" in fields, ( + "TemplateDTO must have a provider_config field for typed provider-specific configuration" + ) + # --------------------------------------------------------------------------- -# from_domain() packs AWS fields into metadata +# from_domain() populates typed provider_config # --------------------------------------------------------------------------- @@ -89,46 +109,62 @@ def _make_aws_template(**kwargs) -> AWSTemplate: return AWSTemplate(**defaults) -class TestFromDomainPacksAWSFieldsIntoMetadata: - """TemplateDTO.from_domain() must move AWS fields into the metadata dict.""" +class TestFromDomainPopulatesProviderConfig: + """TemplateDTO.from_domain() must move AWS fields into the typed provider_config.""" - def test_fleet_type_in_metadata(self): + def test_provider_config_is_aws_dto_config_type(self): template = _make_aws_template(fleet_type=AWSFleetType.MAINTAIN) dto = TemplateDTO.from_domain(template) - assert dto.metadata.get("fleet_type") == "maintain", ( - "fleet_type must be stored in metadata, not as a top-level field" + assert isinstance(dto.provider_config, AWSTemplateDTOConfig), ( + "provider_config must be an AWSTemplateDTOConfig instance for AWS templates" ) - def test_fleet_role_in_metadata(self): + def test_fleet_type_in_provider_config(self): + template = _make_aws_template(fleet_type=AWSFleetType.MAINTAIN) + dto = TemplateDTO.from_domain(template) + assert isinstance(dto.provider_config, AWSTemplateDTOConfig) + assert dto.provider_config.fleet_type == "maintain", ( + "fleet_type must be stored in provider_config, not as a top-level field" + ) + + def test_fleet_role_in_provider_config(self): template = _make_aws_template(fleet_role="arn:aws:iam::123:role/MyRole") dto = TemplateDTO.from_domain(template) - assert dto.metadata.get("fleet_role") == "arn:aws:iam::123:role/MyRole" + assert isinstance(dto.provider_config, AWSTemplateDTOConfig) + assert dto.provider_config.fleet_role == "arn:aws:iam::123:role/MyRole" - def test_percent_on_demand_in_metadata(self): + def test_percent_on_demand_in_provider_config(self): template = _make_aws_template(price_type="heterogeneous", percent_on_demand=40) dto = TemplateDTO.from_domain(template) - assert dto.metadata.get("percent_on_demand") == 40 + assert isinstance(dto.provider_config, AWSTemplateDTOConfig) + assert dto.provider_config.percent_on_demand == 40 + + def test_launch_template_id_in_provider_config(self): + template = _make_aws_template(launch_template_id="lt-0abc123def456") + dto = TemplateDTO.from_domain(template) + assert isinstance(dto.provider_config, AWSTemplateDTOConfig) + assert dto.provider_config.launch_template_id == "lt-0abc123def456" - def test_abis_instance_requirements_in_metadata(self): + def test_abis_instance_requirements_in_provider_config(self): abis = ABISInstanceRequirements( VCpuCount=AWSRequiredIntegerRange(Min=2, Max=8), MemoryMiB=AWSRequiredIntegerRange(Min=4096, Max=16384), ) template = _make_aws_template(abis_instance_requirements=abis) dto = TemplateDTO.from_domain(template) - stored = dto.metadata.get("abis_instance_requirements") - assert stored is not None, "abis_instance_requirements must be stored in metadata" - assert stored["VCpuCount"]["Min"] == 2 - - def test_none_fleet_type_not_in_metadata(self): - """When fleet_type is None it must not pollute metadata.""" + assert isinstance(dto.provider_config, AWSTemplateDTOConfig) + stored = dto.provider_config.abis_instance_requirements + assert stored is not None, "abis_instance_requirements must be stored in provider_config" + # model_dump() serialises ABISInstanceRequirements with snake_case field names + assert stored["vcpu_count"]["min"] == 2 + + def test_none_fleet_type_not_polluting_metadata(self): + """When fleet_type is None on the domain object, metadata must stay clean.""" template = _make_aws_template(provider_api="RunInstances") dto = TemplateDTO.from_domain(template) - assert "fleet_type" not in dto.metadata or dto.metadata.get("fleet_type") is None or True - # More precisely: if fleet_type is None on the domain object, metadata should not - # have a fleet_type key (or it may have one set by AWSTemplate's own validator). - # The key assertion is that no top-level attribute exists. - assert not hasattr(dto, "fleet_type") or "fleet_type" not in dto.model_fields + assert "fleet_type" not in dto.metadata, ( + "fleet_type must not pollute the cross-provider metadata dict" + ) def test_existing_metadata_preserved(self): """from_domain must not discard pre-existing metadata on the domain object.""" @@ -137,8 +173,32 @@ def test_existing_metadata_preserved(self): metadata={"custom_key": "custom_value"}, ) dto = TemplateDTO.from_domain(template) - assert dto.metadata.get("custom_key") == "custom_value" - assert dto.metadata.get("fleet_type") == "request" + assert dto.metadata.get("custom_key") == "custom_value", ( + "Pre-existing metadata keys must be preserved" + ) + # fleet_type must now be in provider_config, NOT metadata + assert "fleet_type" not in dto.metadata, ( + "fleet_type must not pollute the cross-provider metadata dict" + ) + assert isinstance(dto.provider_config, AWSTemplateDTOConfig) + assert dto.provider_config.fleet_type == "request" + + def test_metadata_stays_clean_of_aws_fields(self): + """metadata dict must not contain any AWS-specific keys after from_domain.""" + aws_only_keys = { + "fleet_type", + "fleet_role", + "percent_on_demand", + "abis_instance_requirements", + } + template = _make_aws_template( + fleet_type=AWSFleetType.MAINTAIN, + fleet_role="arn:aws:iam::123:role/R", + percent_on_demand=50, + ) + dto = TemplateDTO.from_domain(template) + leaked = aws_only_keys & set(dto.metadata.keys()) + assert not leaked, f"AWS fields leaked into metadata: {leaked}" # --------------------------------------------------------------------------- @@ -147,7 +207,7 @@ def test_existing_metadata_preserved(self): class TestAWSTemplateRoundTrip: - """AWSTemplate → TemplateDTO → AWSTemplate must preserve AWS fields.""" + """AWSTemplate -> TemplateDTO -> AWSTemplate must preserve AWS fields.""" def test_fleet_type_maintain_roundtrip(self): original = _make_aws_template(fleet_type=AWSFleetType.MAINTAIN) @@ -174,6 +234,12 @@ def test_percent_on_demand_roundtrip(self): restored = AWSTemplate.model_validate(dto.model_dump()) assert restored.percent_on_demand == 60 + def test_launch_template_id_roundtrip(self): + original = _make_aws_template(launch_template_id="lt-0deadbeef") + dto = TemplateDTO.from_domain(original) + restored = AWSTemplate.model_validate(dto.model_dump()) + assert restored.launch_template_id == "lt-0deadbeef" + def test_abis_roundtrip(self): abis = ABISInstanceRequirements( VCpuCount=AWSRequiredIntegerRange(Min=4, Max=16), diff --git a/tests/unit/interface/test_infrastructure_command_handler.py b/tests/unit/interface/test_infrastructure_command_handler.py index 1fc00d25b..677662a8d 100644 --- a/tests/unit/interface/test_infrastructure_command_handler.py +++ b/tests/unit/interface/test_infrastructure_command_handler.py @@ -279,3 +279,170 @@ async def test_unknown_provider_returns_error(self, tmp_path): result = await handle_infrastructure_validate(args) assert result["status"] == "error" + + +# --------------------------------------------------------------------------- +# _get_active_providers — no "aws" hardcoded fallback +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestGetActiveProvidersRegistryFallback: + def test_no_config_file_uses_registry_first_type(self, tmp_path): + """When no config.json exists, use first registered type (not hardcoded 'aws').""" + from unittest.mock import MagicMock, patch + + from orb.interface.infrastructure_command_handler import _get_active_providers + + mock_registry_service = MagicMock() + mock_registry_service.get_registered_provider_types.return_value = ["gcp"] + mock_container = MagicMock() + mock_container.get.return_value = mock_registry_service + + with ( + patch( + "orb.interface.infrastructure_command_handler.get_config_location", + return_value=tmp_path, + ), + patch( + "orb.interface.infrastructure_command_handler.get_container", + return_value=mock_container, + ), + ): + providers = _get_active_providers() + + assert providers[0]["type"] == "gcp" + + def test_no_config_file_no_registered_types_raises(self, tmp_path): + """When no config.json and no registered types, raise RuntimeError.""" + from unittest.mock import MagicMock, patch + + from orb.interface.infrastructure_command_handler import _get_active_providers + + mock_registry_service = MagicMock() + mock_registry_service.get_registered_provider_types.return_value = [] + mock_container = MagicMock() + mock_container.get.return_value = mock_registry_service + + with ( + patch( + "orb.interface.infrastructure_command_handler.get_config_location", + return_value=tmp_path, + ), + patch( + "orb.interface.infrastructure_command_handler.get_container", + return_value=mock_container, + ), + ): + with pytest.raises(RuntimeError, match="No providers are registered"): + _get_active_providers() + + def test_all_providers_disabled_no_registered_types_raises(self, tmp_path): + """When all providers disabled and no registered types, raise RuntimeError.""" + from unittest.mock import MagicMock, patch + + from orb.interface.infrastructure_command_handler import _get_active_providers + + _write_config( + tmp_path, + { + "provider": { + "providers": [ + { + "name": "aws-a", + "type": "aws", + "enabled": False, + } + ] + } + }, + ) + + mock_registry_service = MagicMock() + mock_registry_service.get_registered_provider_types.return_value = [] + mock_container = MagicMock() + mock_container.get.return_value = mock_registry_service + + with ( + patch( + "orb.interface.infrastructure_command_handler.get_config_location", + return_value=tmp_path, + ), + patch( + "orb.interface.infrastructure_command_handler.get_container", + return_value=mock_container, + ), + ): + with pytest.raises(RuntimeError, match="No providers are registered"): + _get_active_providers() + + +# --------------------------------------------------------------------------- +# _show_provider_infrastructure — CLISpecRegistry-driven display +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestShowProviderInfrastructure: + def test_known_provider_type_uses_spec_format_display(self): + """Known provider types use CLISpecRegistry.format_display for display.""" + from unittest.mock import MagicMock, patch + + from orb.infrastructure.registry.cli_spec_registry import CLISpecRegistry + from orb.interface.infrastructure_command_handler import _show_provider_infrastructure + + mock_spec = MagicMock() + mock_spec.format_display.return_value = [("Zone", "us-central1-a"), ("Project", "my-proj")] + mock_console = MagicMock() + mock_container = MagicMock() + mock_container.get.return_value = mock_console + + provider = { + "name": "gcp-main", + "type": "gcp", + "config": {"zone": "us-central1-a", "project": "my-proj"}, + } + + with ( + patch.object(CLISpecRegistry, "get", return_value=mock_spec), + patch( + "orb.interface.infrastructure_command_handler.get_container", + return_value=mock_container, + ), + ): + _show_provider_infrastructure(provider) + + mock_spec.format_display.assert_called_once_with(provider["config"]) + # Ensure format_display output was written to console + info_calls = [call.args[0] for call in mock_console.info.call_args_list] + assert any("Zone" in c for c in info_calls) + assert any("us-central1-a" in c for c in info_calls) + + def test_unknown_provider_type_falls_back_to_raw_config(self): + """Unknown provider types fall back to raw key/value display.""" + from unittest.mock import MagicMock, patch + + from orb.infrastructure.registry.cli_spec_registry import CLISpecRegistry + from orb.interface.infrastructure_command_handler import _show_provider_infrastructure + + mock_console = MagicMock() + mock_container = MagicMock() + mock_container.get.return_value = mock_console + + provider = { + "name": "unknown-1", + "type": "unknown", + "config": {"endpoint": "https://example.com"}, + } + + with ( + patch.object(CLISpecRegistry, "get", return_value=None), + patch( + "orb.interface.infrastructure_command_handler.get_container", + return_value=mock_container, + ), + ): + _show_provider_infrastructure(provider) + + info_calls = [call.args[0] for call in mock_console.info.call_args_list] + assert any("https://example.com" in c for c in info_calls) diff --git a/tests/unit/interface/test_provider_config_handler.py b/tests/unit/interface/test_provider_config_handler.py index 8c2708d9e..8b3225097 100644 --- a/tests/unit/interface/test_provider_config_handler.py +++ b/tests/unit/interface/test_provider_config_handler.py @@ -18,7 +18,7 @@ @pytest.fixture(autouse=True) def register_aws_cli_spec(): """Register AWS CLI spec for all tests in this module.""" - from orb.domain.base.ports.provider_cli_spec_port import CLISpecRegistry + from orb.infrastructure.registry.cli_spec_registry import CLISpecRegistry from orb.providers.aws.cli.aws_cli_spec import AWSCLISpec CLISpecRegistry.register("aws", AWSCLISpec()) @@ -216,6 +216,22 @@ async def test_credential_failure_returns_1(self, tmp_path): assert result.get("error") is True and result.get("exit_code") == 1 + @pytest.mark.asyncio + async def test_missing_provider_type_returns_1(self, tmp_path): + """No provider_type attribute on args must return an error, not default to 'aws'.""" + from orb.interface.provider_config_handler import handle_provider_add + + _write_config(tmp_path, _base_config()) + with patch( + "orb.interface.provider_config_handler.get_config_location", + return_value=tmp_path, + ): + # args has no provider_type attribute at all + args = _ns(aws_profile="default", aws_region="us-east-1", name=None, discover=False) + result = await handle_provider_add(args) + + assert result.get("error") is True and result.get("exit_code") == 1 + # --------------------------------------------------------------------------- # handle_provider_remove diff --git a/tests/unit/providers/aws/infrastructure/handlers/test_ec2_fleet_handler.py b/tests/unit/providers/aws/infrastructure/handlers/test_ec2_fleet_handler.py deleted file mode 100644 index 657182abf..000000000 --- a/tests/unit/providers/aws/infrastructure/handlers/test_ec2_fleet_handler.py +++ /dev/null @@ -1,252 +0,0 @@ -"""Unit tests for EC2FleetHandler.check_hosts_status.""" - -from unittest.mock import MagicMock, patch - -import pytest -from botocore.exceptions import ClientError - -from orb.providers.aws.exceptions.aws_exceptions import AWSInfrastructureError -from orb.providers.aws.infrastructure.handlers.ec2_fleet.handler import EC2FleetHandler - - -def _make_handler(): - aws_client = MagicMock() - logger = MagicMock() - aws_ops = MagicMock() - launch_template_manager = MagicMock() - handler = EC2FleetHandler(aws_client, logger, aws_ops, launch_template_manager) - handler._machine_adapter = None - return handler - - -def _make_request(resource_ids, metadata=None): - request = MagicMock() - request.request_id = "req-ec2fleet-123" - request.resource_ids = resource_ids - request.metadata = metadata or {} - return request - - -def _make_client_error(code="InternalError"): - return ClientError({"Error": {"Code": code, "Message": "boom"}}, "DescribeFleets") - - -def _formatted_instances(instance_ids, resource_id="fleet-test"): - """Return already-formatted instance dicts (as _check_single_fleet_status returns them).""" - return [ - { - "instance_id": iid, - "resource_id": resource_id, - "status": "running", - "private_ip": f"10.0.0.{i}", - "public_ip": None, - "launch_time": None, - "instance_type": "t3.medium", - "image_id": "ami-123", - "subnet_id": None, - "security_group_ids": [], - "vpc_id": None, - } - for i, iid in enumerate(instance_ids) - ] - - -class TestEC2FleetHandlerCheckHostsStatus: - def test_check_hosts_status_all_running(self): - """All instances running → returns all as active.""" - handler = _make_handler() - request = _make_request(["fleet-111"], metadata={"fleet_type": "maintain"}) - instance_ids = ["i-aaa", "i-bbb"] - - with patch.object( - handler, - "_check_single_fleet_status", - return_value=_formatted_instances(instance_ids, "fleet-111"), - ): - result = handler.check_hosts_status(request) - - assert len(result) == 2 - returned_ids = {r["instance_id"] for r in result} - assert returned_ids == set(instance_ids) - - def test_check_hosts_status_mixed_states(self): - """describe_fleet_instances only returns active instances — AWS filters for us.""" - handler = _make_handler() - request = _make_request(["fleet-222"], metadata={"fleet_type": "maintain"}) - active_ids = ["i-running1"] - - with patch.object( - handler, - "_check_single_fleet_status", - return_value=_formatted_instances(active_ids, "fleet-222"), - ): - result = handler.check_hosts_status(request) - - assert len(result) == 1 - assert result[0]["instance_id"] == "i-running1" - - def test_check_hosts_status_fleet_not_found(self): - """_check_single_fleet_status raises → per-fleet exception logged; result is [].""" - handler = _make_handler() - request = _make_request(["fleet-missing"], metadata={"fleet_type": "maintain"}) - - with patch.object( - handler, - "_check_single_fleet_status", - side_effect=AWSInfrastructureError("Fleet not found"), - ): - result = handler.check_hosts_status(request) - - assert result == [] - - def test_check_hosts_status_multiple_resource_ids(self): - """Request has 2 fleet IDs → checks both, aggregates results.""" - handler = _make_handler() - request = _make_request(["fleet-A", "fleet-B"], metadata={"fleet_type": "maintain"}) - - ids_a = ["i-a1", "i-a2"] - ids_b = ["i-b1"] - - def single_fleet_side_effect(fleet_id, req): - if fleet_id == "fleet-A": - return _formatted_instances(ids_a, "fleet-A") - return _formatted_instances(ids_b, "fleet-B") - - with patch.object( - handler, "_check_single_fleet_status", side_effect=single_fleet_side_effect - ): - result = handler.check_hosts_status(request) - - assert len(result) == 3 - returned_ids = {r["instance_id"] for r in result} - assert returned_ids == {"i-a1", "i-a2", "i-b1"} - - def test_check_hosts_status_aws_error(self): - """ClientError inside per-fleet loop → logged and skipped; result is [].""" - handler = _make_handler() - request = _make_request(["fleet-err"], metadata={"fleet_type": "maintain"}) - - with patch.object( - handler, "_check_single_fleet_status", side_effect=_make_client_error("InternalError") - ): - result = handler.check_hosts_status(request) - - assert result == [] - - def test_check_hosts_status_no_resource_ids(self): - """Empty resource_ids → raises AWSInfrastructureError immediately.""" - handler = _make_handler() - request = _make_request([]) - - with pytest.raises(AWSInfrastructureError): - handler.check_hosts_status(request) - - def test_check_hosts_status_returns_correct_count(self): - """Verify count matches active instances returned.""" - handler = _make_handler() - request = _make_request(["fleet-cnt"], metadata={"fleet_type": "maintain"}) - instance_ids = ["i-1", "i-2", "i-3"] - - with patch.object( - handler, - "_check_single_fleet_status", - return_value=_formatted_instances(instance_ids, "fleet-cnt"), - ): - result = handler.check_hosts_status(request) - - assert len(result) == 3 - - def test_check_hosts_status_preserves_instance_ids(self): - """Instance IDs in result match input.""" - handler = _make_handler() - request = _make_request(["fleet-ids"], metadata={"fleet_type": "maintain"}) - instance_ids = ["i-preserve1", "i-preserve2"] - - with patch.object( - handler, - "_check_single_fleet_status", - return_value=_formatted_instances(instance_ids, "fleet-ids"), - ): - result = handler.check_hosts_status(request) - - returned_ids = {r["instance_id"] for r in result} - assert returned_ids == set(instance_ids) - - def test_check_hosts_status_state_filtering_is_strict(self): - """Only instances returned by _check_single_fleet_status appear in result.""" - handler = _make_handler() - request = _make_request(["fleet-strict"], metadata={"fleet_type": "maintain"}) - active_ids = ["i-strict-active"] - - with patch.object( - handler, - "_check_single_fleet_status", - return_value=_formatted_instances(active_ids, "fleet-strict"), - ): - result = handler.check_hosts_status(request) - - assert len(result) == 1 - assert result[0]["instance_id"] == "i-strict-active" - - def test_check_hosts_status_instant_fleet_no_active_instances(self): - """Instant fleet with no instances → _check_single_fleet_status returns [].""" - handler = _make_handler() - request = _make_request( - ["fleet-instant"], - metadata={"fleet_type": "instant", "instance_ids": []}, - ) - - with patch.object(handler, "_check_single_fleet_status", return_value=[]): - result = handler.check_hosts_status(request) - - assert result == [] - - -class TestEC2FleetHandlerNameTag: - def test_fleet_config_instance_tag_uses_config_prefix(self): - """Instance Name tag in EC2Fleet config uses config_port prefix (instant fleet only).""" - from orb.providers.aws.domain.template.aws_template_aggregate import AWSFleetType - - aws_client = MagicMock() - logger = MagicMock() - aws_ops = MagicMock() - launch_template_manager = MagicMock() - config_port = MagicMock() - config_port.get_resource_prefix.side_effect = lambda rt: ( - "pfx-" if rt == "fleet" else "inst-" - ) - - handler = EC2FleetHandler( - aws_client, logger, aws_ops, launch_template_manager, config_port=config_port - ) - - template = MagicMock() - template.fleet_type = AWSFleetType.INSTANT - template.tags = {} - template.price_type = "ondemand" - template.allocation_strategy = None - template.max_price = None - template.machine_types = {"m5.large": 1} - template.subnet_ids = ["subnet-abc"] - template.template_id = "tmpl-ec2" - template.percent_on_demand = None - template.context = None - - request = MagicMock() - request.request_id = "req-ec2-001" - request.requested_count = 2 - - fleet_config = handler._create_fleet_config(template, request, "lt-xyz", "$Default") - - instance_ts = next( - ( - ts - for ts in fleet_config.get("TagSpecifications", []) - if ts["ResourceType"] == "instance" - ), - None, - ) - assert instance_ts is not None, "No instance TagSpecification found (instant fleet)" - name_tag = next((t for t in instance_ts["Tags"] if t["Key"] == "Name"), None) - assert name_tag is not None, "No Name tag in instance TagSpecification" - assert "req-ec2-001" in name_tag["Value"] diff --git a/tests/unit/providers/test_provider_registry.py b/tests/unit/providers/test_provider_registry.py index 5685ac5e6..a0235fc3e 100644 --- a/tests/unit/providers/test_provider_registry.py +++ b/tests/unit/providers/test_provider_registry.py @@ -1,7 +1,7 @@ -"""Tests for ProviderRegistry._provider_supports_api — no hardcoded AWS API list.""" +"""Tests for ProviderRegistry — no hardcoded AWS API list and provider_type allowlist.""" from typing import cast -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest @@ -160,3 +160,123 @@ def test_capabilities_instance_attr_still_takes_precedence(self): registry = _make_registry(provider, strategy=strategy) assert registry._provider_supports_api(provider, "SpotFleet") is True + + +# --------------------------------------------------------------------------- +# Provider type allowlist — ensure_provider_type_registered and +# ensure_provider_instance_registered_from_config must reject provider_type +# values that would allow module-injection via importlib.import_module. +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestProviderTypeAllowlist: + """ensure_provider_type_registered must reject non-alphanumeric provider types.""" + + def _make_bare_registry(self) -> ProviderRegistry: + registry = cast(ProviderRegistry, ProviderRegistry.__new__(ProviderRegistry)) + registry._type_registrations = {} + registry._instance_registrations = {} + registry._registry_lock = __import__("threading").RLock() + registry.mode = __import__( + "orb.infrastructure.registry.base_registry", fromlist=["RegistryMode"] + ).RegistryMode.MULTI_CHOICE + registry._factory = None + registry._initialized = True + registry._strategy_cache = {} + registry._health_states = {} + registry._fallback_strategy = None + registry._config_port = None + registry._logger = MagicMock() + return registry + + # --- ensure_provider_type_registered --- + + def test_valid_provider_type_attempts_import(self): + """A valid snake_case type like 'aws' attempts the dynamic import (no ValueError).""" + registry = self._make_bare_registry() + + with patch("importlib.import_module", side_effect=ImportError("no module")) as mock_import: + result = registry.ensure_provider_type_registered("aws") + + # ImportError → returns False, but the import WAS attempted (no ValueError raised) + mock_import.assert_called_once_with("orb.providers.aws.registration") + assert result is False + + def test_valid_provider_type_with_underscores(self): + """Types like 'my_provider' pass the allowlist.""" + registry = self._make_bare_registry() + + with patch("importlib.import_module", side_effect=ImportError("no module")): + # Should not raise ValueError + registry.ensure_provider_type_registered("my_provider") + + def test_valid_provider_type_with_digits(self): + """Types like 'provider1' pass the allowlist.""" + registry = self._make_bare_registry() + + with patch("importlib.import_module", side_effect=ImportError("no module")): + registry.ensure_provider_type_registered("provider1") + + def test_dot_in_provider_type_raises_value_error(self): + """A provider_type containing a dot (e.g. 'os.path') must raise ValueError.""" + registry = self._make_bare_registry() + + with pytest.raises(ValueError, match="Invalid provider type"): + registry.ensure_provider_type_registered("os.path") + + def test_path_traversal_raises_value_error(self): + """A path-traversal string must raise ValueError.""" + registry = self._make_bare_registry() + + with pytest.raises(ValueError, match="Invalid provider type"): + registry.ensure_provider_type_registered("../../etc/passwd") + + def test_uppercase_raises_value_error(self): + """Uppercase letters are not permitted (consistent snake_case convention).""" + registry = self._make_bare_registry() + + with pytest.raises(ValueError, match="Invalid provider type"): + registry.ensure_provider_type_registered("AWS") + + def test_leading_digit_raises_value_error(self): + """A provider_type starting with a digit must raise ValueError.""" + registry = self._make_bare_registry() + + with pytest.raises(ValueError, match="Invalid provider type"): + registry.ensure_provider_type_registered("1provider") + + def test_space_in_provider_type_raises_value_error(self): + """A provider_type with whitespace must raise ValueError.""" + registry = self._make_bare_registry() + + with pytest.raises(ValueError, match="Invalid provider type"): + registry.ensure_provider_type_registered("aws provider") + + # --- ensure_provider_instance_registered_from_config --- + + def test_instance_registration_dot_in_type_raises_value_error(self): + """ensure_provider_instance_registered_from_config rejects dotted provider types.""" + registry = self._make_bare_registry() + + provider_instance = MagicMock() + provider_instance.name = "my-instance" + provider_instance.type = "malicious.module" + # Not yet registered + registry._instance_registrations = {} + + with pytest.raises(ValueError, match="Invalid provider type"): + registry.ensure_provider_instance_registered_from_config(provider_instance) + + def test_instance_registration_valid_type_attempts_import(self): + """ensure_provider_instance_registered_from_config passes validation for 'aws'.""" + registry = self._make_bare_registry() + + provider_instance = MagicMock() + provider_instance.name = "aws-east" + provider_instance.type = "aws" + + with patch("importlib.import_module", side_effect=ImportError("no module")): + result = registry.ensure_provider_instance_registered_from_config(provider_instance) + + assert result is False # ImportError → False, but no ValueError diff --git a/tests/unit/providers/test_scenarios_helpers.py b/tests/unit/providers/test_scenarios_helpers.py new file mode 100644 index 000000000..d86d0ac9e --- /dev/null +++ b/tests/unit/providers/test_scenarios_helpers.py @@ -0,0 +1,60 @@ +"""Smoke tests for helper functions in tests/providers/aws/live/scenarios.py.""" + +from tests.providers.aws.live.scenarios import _load_template_vm_types + + +class TestLoadTemplateVmTypes: + def test_known_template_returns_nonempty_dict(self) -> None: + """EC2Fleet-Maintain-OnDemand exists in config/aws_templates.json and has vmTypes.""" + result = _load_template_vm_types("EC2Fleet-Maintain-OnDemand") + assert isinstance(result, dict), "Expected a dict" + assert result, "Expected a non-empty dict for EC2Fleet-Maintain-OnDemand" + + def test_unknown_template_returns_empty_dict(self) -> None: + """An unrecognised template ID should silently return {}.""" + result = _load_template_vm_types("DoesNotExist-Template-ID") + assert result == {} + + def test_file_not_found_returns_empty_dict(self, monkeypatch) -> None: + """FileNotFoundError during open() is swallowed and returns {}.""" + import builtins + from pathlib import Path + + def fake_open(path, *args, **kwargs): + raise FileNotFoundError(f"injected: {path}") + + monkeypatch.setattr(builtins, "open", fake_open) + # templates_path.exists() must return True to reach the open() call + monkeypatch.setattr(Path, "exists", lambda self: True) + result = _load_template_vm_types("EC2Fleet-Maintain-OnDemand") + assert result == {} + + def test_json_decode_error_returns_empty_dict(self, monkeypatch) -> None: + """json.JSONDecodeError during json.load() is swallowed and returns {}.""" + import builtins + import io + from pathlib import Path + + def fake_open(path, *args, **kwargs): + return io.StringIO("<<>>") + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(Path, "exists", lambda self: True) + result = _load_template_vm_types("EC2Fleet-Maintain-OnDemand") + assert result == {} + + def test_unexpected_exception_is_reraised(self, monkeypatch) -> None: + """Unexpected exceptions (not FileNotFoundError/JSONDecodeError) are re-raised.""" + import builtins + + def exploding_open(path, *args, **kwargs): + raise RuntimeError("disk on fire") + + monkeypatch.setattr(builtins, "open", exploding_open) + from pathlib import Path + + monkeypatch.setattr(Path, "exists", lambda self: True) + import pytest + + with pytest.raises(RuntimeError, match="disk on fire"): + _load_template_vm_types("EC2Fleet-Maintain-OnDemand") diff --git a/tests/unit/sdk/test_sdk_config.py b/tests/unit/sdk/test_sdk_config.py index 838000288..7827a778a 100644 --- a/tests/unit/sdk/test_sdk_config.py +++ b/tests/unit/sdk/test_sdk_config.py @@ -102,6 +102,30 @@ def test_empty_dict_uses_defaults(self): config = SDKConfig.from_dict({}) assert config.provider == "aws" + def test_scheduler_dict_not_absorbed_as_string_override(self): + # Regression: when loading from an ORB config.json the top-level "scheduler" key + # is a nested config dict {"type": "hostfactory", "config_root": "..."}. + # from_dict must NOT ingest that dict as the SDKConfig.scheduler string override + # because that propagates a dict into ConfigurationManager.override_scheduler_strategy + # which then surfaces as "unhashable type: 'dict'" deep in the DI factory chain. + orb_config = { + "provider": {"type": "aws"}, + "scheduler": {"type": "hostfactory", "config_root": "$ORB_CONFIG_DIR"}, + } + config = SDKConfig.from_dict(orb_config) + # scheduler field must remain None (no string override set) + assert config.scheduler is None + # the original dict is preserved in custom_config so nothing is silently dropped + assert config.custom_config.get("scheduler") == { + "type": "hostfactory", + "config_root": "$ORB_CONFIG_DIR", + } + + def test_scheduler_string_override_is_still_accepted(self): + # Explicit string scheduler overrides (e.g. passed programmatically) must still work. + config = SDKConfig.from_dict({"provider": "aws", "scheduler": "default"}) + assert config.scheduler == "default" + class TestSDKConfigFromFile: def test_loads_json_file(self, tmp_path): diff --git a/tests/unit/test_health_no_aws.py b/tests/unit/test_health_no_aws.py index d5ac6fa32..c35a0c63a 100644 --- a/tests/unit/test_health_no_aws.py +++ b/tests/unit/test_health_no_aws.py @@ -40,11 +40,11 @@ def test_register_aws_health_checks_callable(): ) -# --- task 1719: register_aws_health_checks registers checks on HealthCheck --- +# --- register_aws_health_checks: conditional DynamoDB registration --- -def test_register_aws_health_checks_registers_checks(): - """register_aws_health_checks must add aws, ec2, and dynamodb checks.""" +def test_register_aws_health_checks_always_registers_aws_and_ec2(): + """register_aws_health_checks must always add aws and ec2 checks.""" from unittest.mock import MagicMock from orb.providers.aws.health import register_aws_health_checks @@ -52,10 +52,44 @@ def test_register_aws_health_checks_registers_checks(): health_check = MagicMock() aws_client = MagicMock() - register_aws_health_checks(health_check, aws_client) + register_aws_health_checks(health_check, aws_client, storage_strategy="json") + + registered_names = {call.args[0] for call in health_check.register_check.call_args_list} + assert "aws" in registered_names + assert "ec2" in registered_names + assert "dynamodb" not in registered_names + assert health_check.register_check.call_count == 2 + + +def test_register_aws_health_checks_registers_dynamodb_when_dynamodb_storage(): + """register_aws_health_checks must add dynamodb check only when storage_strategy='dynamodb'.""" + from unittest.mock import MagicMock + + from orb.providers.aws.health import register_aws_health_checks + + health_check = MagicMock() + aws_client = MagicMock() + + register_aws_health_checks(health_check, aws_client, storage_strategy="dynamodb") registered_names = {call.args[0] for call in health_check.register_check.call_args_list} assert "aws" in registered_names assert "ec2" in registered_names assert "dynamodb" in registered_names assert health_check.register_check.call_count == 3 + + +def test_register_aws_health_checks_skips_dynamodb_for_sql_storage(): + """register_aws_health_checks must not add dynamodb check when storage_strategy='sql'.""" + from unittest.mock import MagicMock + + from orb.providers.aws.health import register_aws_health_checks + + health_check = MagicMock() + aws_client = MagicMock() + + register_aws_health_checks(health_check, aws_client, storage_strategy="sql") + + registered_names = {call.args[0] for call in health_check.register_check.call_args_list} + assert "dynamodb" not in registered_names + assert health_check.register_check.call_count == 2 diff --git a/tests/unit/test_no_provider_install.py b/tests/unit/test_no_provider_install.py new file mode 100644 index 000000000..da37ff7f8 --- /dev/null +++ b/tests/unit/test_no_provider_install.py @@ -0,0 +1,161 @@ +"""Verify ORB core boots cleanly when the [aws] extra is not installed. + +These tests use subprocess + import mocking to simulate a boto3-free environment +without requiring a separate virtualenv. The approach patches sys.modules to make +boto3/botocore appear absent, then imports ORB core modules under that constraint. + +Marked @pytest.mark.slow because they manipulate sys.modules and must restore state. +""" + +from __future__ import annotations + +import importlib +import sys +from types import ModuleType +from unittest.mock import patch + +import pytest + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class _ImportBlocker(ModuleType): + """A fake module that raises ImportError on attribute access, simulating absence.""" + + def __getattr__(self, name: str) -> object: + raise ImportError("boto3 extra not installed (simulated)") + + +def _block_modules(*names: str) -> dict[str, ModuleType | None]: + """Return a dict suitable for patching sys.modules to block *names*.""" + return {name: None for name in names} # type: ignore[dict-item] + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +@pytest.mark.slow +def test_cleanup_schema_import_without_aws() -> None: + """cleanup_schema must import without error even when boto3 is absent.""" + blocked = { + "boto3": None, + "botocore": None, + "orb.providers.aws.configuration.cleanup_config": None, + } + # Remove cached module if present + cached = {k: sys.modules.pop(k, None) for k in blocked} + cached.pop("orb.config.schemas.cleanup_schema", None) + sys.modules.pop("orb.config.schemas.cleanup_schema", None) + + try: + with patch.dict(sys.modules, blocked): + import importlib + + mod = importlib.import_module("orb.config.schemas.cleanup_schema") + # CleanupConfig should be None when AWS extra is absent + assert mod.CleanupConfig is None, ( + "CleanupConfig should be None when [aws] extra is absent" + ) + assert mod.CleanupResourcesConfig is None, ( + "CleanupResourcesConfig should be None when [aws] extra is absent" + ) + finally: + # Restore originals so other tests are unaffected + for k, v in cached.items(): + if v is not None: + sys.modules[k] = v + else: + sys.modules.pop(k, None) + sys.modules.pop("orb.config.schemas.cleanup_schema", None) + + +@pytest.mark.slow +def test_storage_registration_without_aws() -> None: + """register_all_storage_types must not raise when AWS DynamoDB import fails.""" + blocked = { + "boto3": None, + "botocore": None, + "orb.providers.aws.storage.registration": None, + } + cached = {k: sys.modules.pop(k, None) for k in blocked} + sys.modules.pop("orb.infrastructure.storage.registration", None) + + try: + with patch.dict(sys.modules, blocked): + mod = importlib.import_module("orb.infrastructure.storage.registration") + # Should not raise — DynamoDB registration is silently skipped + try: + mod.register_all_storage_types() + except ImportError as exc: + pytest.fail( + f"register_all_storage_types() raised ImportError when [aws] absent: {exc}" + ) + finally: + for k, v in cached.items(): + if v is not None: + sys.modules[k] = v + else: + sys.modules.pop(k, None) + sys.modules.pop("orb.infrastructure.storage.registration", None) + + +@pytest.mark.slow +def test_provider_cli_specs_without_aws() -> None: + """register_all_provider_cli_specs must not raise when [aws] is absent.""" + blocked = { + "boto3": None, + "botocore": None, + "orb.providers.aws.cli.aws_cli_spec": None, + } + cached = {k: sys.modules.pop(k, None) for k in blocked} + sys.modules.pop("orb.providers.registration", None) + + try: + with patch.dict(sys.modules, blocked): + mod = importlib.import_module("orb.providers.registration") + try: + mod.register_all_provider_cli_specs() + except ImportError as exc: + pytest.fail( + f"register_all_provider_cli_specs() raised ImportError when [aws] absent: {exc}" + ) + finally: + for k, v in cached.items(): + if v is not None: + sys.modules[k] = v + else: + sys.modules.pop(k, None) + sys.modules.pop("orb.providers.registration", None) + + +@pytest.mark.slow +def test_provider_defaults_loaders_without_aws() -> None: + """register_all_defaults_loaders must not raise when [aws] is absent.""" + blocked = { + "boto3": None, + "botocore": None, + "orb.providers.aws.defaults_loader": None, + } + cached = {k: sys.modules.pop(k, None) for k in blocked} + sys.modules.pop("orb.providers.registration", None) + + try: + with patch.dict(sys.modules, blocked): + mod = importlib.import_module("orb.providers.registration") + try: + mod.register_all_defaults_loaders() + except ImportError as exc: + pytest.fail( + f"register_all_defaults_loaders() raised ImportError when [aws] absent: {exc}" + ) + finally: + for k, v in cached.items(): + if v is not None: + sys.modules[k] = v + else: + sys.modules.pop(k, None) + sys.modules.pop("orb.providers.registration", None) diff --git a/uv.lock b/uv.lock index 998504f9a..5cec048f7 100644 --- a/uv.lock +++ b/uv.lock @@ -10,289 +10,289 @@ resolution-markers = [ [[package]] name = "annotated-doc" version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "antlr4-python3-runtime" version = "4.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/5f/2cdf6f7aca3b20d3f316e9f505292e1f256a32089bd702034c29ebde6242/antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916", size = 117467, upload-time = "2024-08-03T19:00:12.757Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/33/5f/2cdf6f7aca3b20d3f316e9f505292e1f256a32089bd702034c29ebde6242/antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916", size = 117467, upload-time = "2024-08-03T19:00:12.757Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/03/a851e84fcbb85214dc637b6378121ef9a0dd61b4c65264675d8a5c9b1ae7/antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8", size = 144462, upload-time = "2024-08-03T19:00:11.134Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/89/03/a851e84fcbb85214dc637b6378121ef9a0dd61b4c65264675d8a5c9b1ae7/antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8", size = 144462, upload-time = "2024-08-03T19:00:11.134Z" }, ] [[package]] name = "anyio" version = "4.14.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, ] [[package]] name = "arrow" version = "1.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "python-dateutil" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, ] [[package]] name = "asgiref" version = "3.11.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, ] [[package]] name = "asttokens" version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] [[package]] name = "attrs" version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] name = "authlib" version = "1.7.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "cryptography" }, { name = "joserfc" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, ] [[package]] name = "aws-sam-translator" version = "1.110.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "boto3" }, { name = "jsonschema" }, { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/2f/adeed2ce2bc62eca7ead7b3ae70fdd2cf84eecd582cd69a9529e6da89876/aws_sam_translator-1.110.0.tar.gz", hash = "sha256:466ee0e8200992c51b7fd5ede5e56ca2e8dd5473cc551e8495c14f2f4d636127", size = 368671, upload-time = "2026-05-19T21:21:06.959Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6e/2f/adeed2ce2bc62eca7ead7b3ae70fdd2cf84eecd582cd69a9529e6da89876/aws_sam_translator-1.110.0.tar.gz", hash = "sha256:466ee0e8200992c51b7fd5ede5e56ca2e8dd5473cc551e8495c14f2f4d636127", size = 368671, upload-time = "2026-05-19T21:21:06.959Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/6f/286e3e49d3b6b181473fefa5d9fc02e10d98ccc417e0de74e396db951fd9/aws_sam_translator-1.110.0-py3-none-any.whl", hash = "sha256:69b09aacf2d305ac747037b7b913224cb8a9d653f47a0306509c1d20e420b670", size = 431671, upload-time = "2026-05-19T21:21:05.26Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/6f/286e3e49d3b6b181473fefa5d9fc02e10d98ccc417e0de74e396db951fd9/aws_sam_translator-1.110.0-py3-none-any.whl", hash = "sha256:69b09aacf2d305ac747037b7b913224cb8a9d653f47a0306509c1d20e420b670", size = 431671, upload-time = "2026-05-19T21:21:05.26Z" }, ] [[package]] name = "aws-xray-sdk" version = "2.15.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "botocore" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/25/0cbd7a440080def5e6f063720c3b190a25f8aa2938c1e34415dc18241596/aws_xray_sdk-2.15.0.tar.gz", hash = "sha256:794381b96e835314345068ae1dd3b9120bd8b4e21295066c37e8814dbb341365", size = 76315, upload-time = "2025-10-29T20:59:45Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/14/25/0cbd7a440080def5e6f063720c3b190a25f8aa2938c1e34415dc18241596/aws_xray_sdk-2.15.0.tar.gz", hash = "sha256:794381b96e835314345068ae1dd3b9120bd8b4e21295066c37e8814dbb341365", size = 76315, upload-time = "2025-10-29T20:59:45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/c3/f30a7a63e664acc7c2545ca0491b6ce8264536e0e5cad3965f1d1b91e960/aws_xray_sdk-2.15.0-py2.py3-none-any.whl", hash = "sha256:422d62ad7d52e373eebb90b642eb1bb24657afe03b22a8df4a8b2e5108e278a3", size = 103228, upload-time = "2025-10-29T21:00:24.12Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ef/c3/f30a7a63e664acc7c2545ca0491b6ce8264536e0e5cad3965f1d1b91e960/aws_xray_sdk-2.15.0-py2.py3-none-any.whl", hash = "sha256:422d62ad7d52e373eebb90b642eb1bb24657afe03b22a8df4a8b2e5108e278a3", size = 103228, upload-time = "2025-10-29T21:00:24.12Z" }, ] [[package]] name = "babel" version = "2.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "backports-asyncio-runner" version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] [[package]] name = "backports-datetime-fromisoformat" version = "2.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/81/eff3184acb1d9dc3ce95a98b6f3c81a49b4be296e664db8e1c2eeabef3d9/backports_datetime_fromisoformat-2.0.3.tar.gz", hash = "sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d", size = 23588, upload-time = "2024-12-28T20:18:15.017Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/4b/d6b051ca4b3d76f23c2c436a9669f3be616b8cf6461a7e8061c7c4269642/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4", size = 27561, upload-time = "2024-12-28T20:16:47.974Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/e39b0d471e55eb1b5c7c81edab605c02f71c786d59fb875f0a6f23318747/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c", size = 34448, upload-time = "2024-12-28T20:16:50.712Z" }, - { url = "https://files.pythonhosted.org/packages/f2/28/7a5c87c5561d14f1c9af979231fdf85d8f9fad7a95ff94e56d2205e2520a/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b", size = 27093, upload-time = "2024-12-28T20:16:52.994Z" }, - { url = "https://files.pythonhosted.org/packages/80/ba/f00296c5c4536967c7d1136107fdb91c48404fe769a4a6fd5ab045629af8/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4", size = 52836, upload-time = "2024-12-28T20:16:55.283Z" }, - { url = "https://files.pythonhosted.org/packages/e3/92/bb1da57a069ddd601aee352a87262c7ae93467e66721d5762f59df5021a6/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22", size = 52798, upload-time = "2024-12-28T20:16:56.64Z" }, - { url = "https://files.pythonhosted.org/packages/df/ef/b6cfd355982e817ccdb8d8d109f720cab6e06f900784b034b30efa8fa832/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf", size = 52891, upload-time = "2024-12-28T20:16:58.887Z" }, - { url = "https://files.pythonhosted.org/packages/37/39/b13e3ae8a7c5d88b68a6e9248ffe7066534b0cfe504bf521963e61b6282d/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58", size = 52955, upload-time = "2024-12-28T20:17:00.028Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e4/70cffa3ce1eb4f2ff0c0d6f5d56285aacead6bd3879b27a2ba57ab261172/backports_datetime_fromisoformat-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc", size = 29323, upload-time = "2024-12-28T20:17:01.125Z" }, - { url = "https://files.pythonhosted.org/packages/62/f5/5bc92030deadf34c365d908d4533709341fb05d0082db318774fdf1b2bcb/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443", size = 27626, upload-time = "2024-12-28T20:17:03.448Z" }, - { url = "https://files.pythonhosted.org/packages/28/45/5885737d51f81dfcd0911dd5c16b510b249d4c4cf6f4a991176e0358a42a/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0", size = 34588, upload-time = "2024-12-28T20:17:04.459Z" }, - { url = "https://files.pythonhosted.org/packages/bc/6d/bd74de70953f5dd3e768c8fc774af942af0ce9f211e7c38dd478fa7ea910/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005", size = 27162, upload-time = "2024-12-28T20:17:06.752Z" }, - { url = "https://files.pythonhosted.org/packages/47/ba/1d14b097f13cce45b2b35db9898957578b7fcc984e79af3b35189e0d332f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75", size = 54482, upload-time = "2024-12-28T20:17:08.15Z" }, - { url = "https://files.pythonhosted.org/packages/25/e9/a2a7927d053b6fa148b64b5e13ca741ca254c13edca99d8251e9a8a09cfe/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45", size = 54362, upload-time = "2024-12-28T20:17:10.605Z" }, - { url = "https://files.pythonhosted.org/packages/c1/99/394fb5e80131a7d58c49b89e78a61733a9994885804a0bb582416dd10c6f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01", size = 54162, upload-time = "2024-12-28T20:17:12.301Z" }, - { url = "https://files.pythonhosted.org/packages/88/25/1940369de573c752889646d70b3fe8645e77b9e17984e72a554b9b51ffc4/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6", size = 54118, upload-time = "2024-12-28T20:17:13.609Z" }, - { url = "https://files.pythonhosted.org/packages/b7/46/f275bf6c61683414acaf42b2df7286d68cfef03e98b45c168323d7707778/backports_datetime_fromisoformat-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061", size = 29329, upload-time = "2024-12-28T20:17:16.124Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/69bbdde2e1e57c09b5f01788804c50e68b29890aada999f2b1a40519def9/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66ce47ee1ba91e146149cf40565c3d750ea1be94faf660ca733d8601e0848147", size = 27630, upload-time = "2024-12-28T20:17:19.442Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1d/1c84a50c673c87518b1adfeafcfd149991ed1f7aedc45d6e5eac2f7d19d7/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8b7e069910a66b3bba61df35b5f879e5253ff0821a70375b9daf06444d046fa4", size = 34707, upload-time = "2024-12-28T20:17:21.79Z" }, - { url = "https://files.pythonhosted.org/packages/71/44/27eae384e7e045cda83f70b551d04b4a0b294f9822d32dea1cbf1592de59/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:a3b5d1d04a9e0f7b15aa1e647c750631a873b298cdd1255687bb68779fe8eb35", size = 27280, upload-time = "2024-12-28T20:17:24.503Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7a/a4075187eb6bbb1ff6beb7229db5f66d1070e6968abeb61e056fa51afa5e/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1b95986430e789c076610aea704db20874f0781b8624f648ca9fb6ef67c6e1", size = 55094, upload-time = "2024-12-28T20:17:25.546Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/3fced4230c10af14aacadc195fe58e2ced91d011217b450c2e16a09a98c8/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffe5f793db59e2f1d45ec35a1cf51404fdd69df9f6952a0c87c3060af4c00e32", size = 55605, upload-time = "2024-12-28T20:17:29.208Z" }, - { url = "https://files.pythonhosted.org/packages/f6/0a/4b34a838c57bd16d3e5861ab963845e73a1041034651f7459e9935289cfd/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:620e8e73bd2595dfff1b4d256a12b67fce90ece3de87b38e1dde46b910f46f4d", size = 55353, upload-time = "2024-12-28T20:17:32.433Z" }, - { url = "https://files.pythonhosted.org/packages/d9/68/07d13c6e98e1cad85606a876367ede2de46af859833a1da12c413c201d78/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4cf9c0a985d68476c1cabd6385c691201dda2337d7453fb4da9679ce9f23f4e7", size = 55298, upload-time = "2024-12-28T20:17:34.919Z" }, - { url = "https://files.pythonhosted.org/packages/60/33/45b4d5311f42360f9b900dea53ab2bb20a3d61d7f9b7c37ddfcb3962f86f/backports_datetime_fromisoformat-2.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:d144868a73002e6e2e6fef72333e7b0129cecdd121aa8f1edba7107fd067255d", size = 29375, upload-time = "2024-12-28T20:17:36.018Z" }, - { url = "https://files.pythonhosted.org/packages/be/03/7eaa9f9bf290395d57fd30d7f1f2f9dff60c06a31c237dc2beb477e8f899/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039", size = 28980, upload-time = "2024-12-28T20:18:06.554Z" }, - { url = "https://files.pythonhosted.org/packages/47/80/a0ecf33446c7349e79f54cc532933780341d20cff0ee12b5bfdcaa47067e/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06", size = 28449, upload-time = "2024-12-28T20:18:07.77Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/71/81/eff3184acb1d9dc3ce95a98b6f3c81a49b4be296e664db8e1c2eeabef3d9/backports_datetime_fromisoformat-2.0.3.tar.gz", hash = "sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d", size = 23588, upload-time = "2024-12-28T20:18:15.017Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/4b/d6b051ca4b3d76f23c2c436a9669f3be616b8cf6461a7e8061c7c4269642/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4", size = 27561, upload-time = "2024-12-28T20:16:47.974Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6d/40/e39b0d471e55eb1b5c7c81edab605c02f71c786d59fb875f0a6f23318747/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c", size = 34448, upload-time = "2024-12-28T20:16:50.712Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f2/28/7a5c87c5561d14f1c9af979231fdf85d8f9fad7a95ff94e56d2205e2520a/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b", size = 27093, upload-time = "2024-12-28T20:16:52.994Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/80/ba/f00296c5c4536967c7d1136107fdb91c48404fe769a4a6fd5ab045629af8/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4", size = 52836, upload-time = "2024-12-28T20:16:55.283Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/92/bb1da57a069ddd601aee352a87262c7ae93467e66721d5762f59df5021a6/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22", size = 52798, upload-time = "2024-12-28T20:16:56.64Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/df/ef/b6cfd355982e817ccdb8d8d109f720cab6e06f900784b034b30efa8fa832/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf", size = 52891, upload-time = "2024-12-28T20:16:58.887Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/37/39/b13e3ae8a7c5d88b68a6e9248ffe7066534b0cfe504bf521963e61b6282d/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58", size = 52955, upload-time = "2024-12-28T20:17:00.028Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1e/e4/70cffa3ce1eb4f2ff0c0d6f5d56285aacead6bd3879b27a2ba57ab261172/backports_datetime_fromisoformat-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc", size = 29323, upload-time = "2024-12-28T20:17:01.125Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/62/f5/5bc92030deadf34c365d908d4533709341fb05d0082db318774fdf1b2bcb/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443", size = 27626, upload-time = "2024-12-28T20:17:03.448Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/28/45/5885737d51f81dfcd0911dd5c16b510b249d4c4cf6f4a991176e0358a42a/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0", size = 34588, upload-time = "2024-12-28T20:17:04.459Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bc/6d/bd74de70953f5dd3e768c8fc774af942af0ce9f211e7c38dd478fa7ea910/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005", size = 27162, upload-time = "2024-12-28T20:17:06.752Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/47/ba/1d14b097f13cce45b2b35db9898957578b7fcc984e79af3b35189e0d332f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75", size = 54482, upload-time = "2024-12-28T20:17:08.15Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/25/e9/a2a7927d053b6fa148b64b5e13ca741ca254c13edca99d8251e9a8a09cfe/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45", size = 54362, upload-time = "2024-12-28T20:17:10.605Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/99/394fb5e80131a7d58c49b89e78a61733a9994885804a0bb582416dd10c6f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01", size = 54162, upload-time = "2024-12-28T20:17:12.301Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/88/25/1940369de573c752889646d70b3fe8645e77b9e17984e72a554b9b51ffc4/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6", size = 54118, upload-time = "2024-12-28T20:17:13.609Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b7/46/f275bf6c61683414acaf42b2df7286d68cfef03e98b45c168323d7707778/backports_datetime_fromisoformat-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061", size = 29329, upload-time = "2024-12-28T20:17:16.124Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a2/0f/69bbdde2e1e57c09b5f01788804c50e68b29890aada999f2b1a40519def9/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66ce47ee1ba91e146149cf40565c3d750ea1be94faf660ca733d8601e0848147", size = 27630, upload-time = "2024-12-28T20:17:19.442Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/1d/1c84a50c673c87518b1adfeafcfd149991ed1f7aedc45d6e5eac2f7d19d7/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8b7e069910a66b3bba61df35b5f879e5253ff0821a70375b9daf06444d046fa4", size = 34707, upload-time = "2024-12-28T20:17:21.79Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/71/44/27eae384e7e045cda83f70b551d04b4a0b294f9822d32dea1cbf1592de59/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:a3b5d1d04a9e0f7b15aa1e647c750631a873b298cdd1255687bb68779fe8eb35", size = 27280, upload-time = "2024-12-28T20:17:24.503Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a7/7a/a4075187eb6bbb1ff6beb7229db5f66d1070e6968abeb61e056fa51afa5e/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1b95986430e789c076610aea704db20874f0781b8624f648ca9fb6ef67c6e1", size = 55094, upload-time = "2024-12-28T20:17:25.546Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/71/03/3fced4230c10af14aacadc195fe58e2ced91d011217b450c2e16a09a98c8/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffe5f793db59e2f1d45ec35a1cf51404fdd69df9f6952a0c87c3060af4c00e32", size = 55605, upload-time = "2024-12-28T20:17:29.208Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f6/0a/4b34a838c57bd16d3e5861ab963845e73a1041034651f7459e9935289cfd/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:620e8e73bd2595dfff1b4d256a12b67fce90ece3de87b38e1dde46b910f46f4d", size = 55353, upload-time = "2024-12-28T20:17:32.433Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d9/68/07d13c6e98e1cad85606a876367ede2de46af859833a1da12c413c201d78/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4cf9c0a985d68476c1cabd6385c691201dda2337d7453fb4da9679ce9f23f4e7", size = 55298, upload-time = "2024-12-28T20:17:34.919Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/33/45b4d5311f42360f9b900dea53ab2bb20a3d61d7f9b7c37ddfcb3962f86f/backports_datetime_fromisoformat-2.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:d144868a73002e6e2e6fef72333e7b0129cecdd121aa8f1edba7107fd067255d", size = 29375, upload-time = "2024-12-28T20:17:36.018Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/be/03/7eaa9f9bf290395d57fd30d7f1f2f9dff60c06a31c237dc2beb477e8f899/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039", size = 28980, upload-time = "2024-12-28T20:18:06.554Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/47/80/a0ecf33446c7349e79f54cc532933780341d20cff0ee12b5bfdcaa47067e/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06", size = 28449, upload-time = "2024-12-28T20:18:07.77Z" }, ] [[package]] name = "backports-tarfile" version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] [[package]] name = "backrefs" version = "7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, - { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, - { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, ] [[package]] name = "bandit" version = "1.9.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "pyyaml" }, { name = "rich" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, ] [[package]] name = "bandit-sarif-formatter" version = "1.1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "jschema-to-python" }, { name = "sarif-om" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/2d/fa7f7769e487ec03a28a62b780f1c0a2e788359ef1a18b8a62744d42ee43/bandit_sarif_formatter-1.1.1.tar.gz", hash = "sha256:2b6299617c559e41cccf4c9cdf0724dbfc9dc276ecbd94c28074db0d3363ef72", size = 7340, upload-time = "2019-10-05T20:24:04.638Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a1/2d/fa7f7769e487ec03a28a62b780f1c0a2e788359ef1a18b8a62744d42ee43/bandit_sarif_formatter-1.1.1.tar.gz", hash = "sha256:2b6299617c559e41cccf4c9cdf0724dbfc9dc276ecbd94c28074db0d3363ef72", size = 7340, upload-time = "2019-10-05T20:24:04.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/9d/4e633743766b159d20cca63fc56c0817633d0f99c49607d72b796a386d46/bandit_sarif_formatter-1.1.1-py3-none-any.whl", hash = "sha256:2a8351cecc03b265ec7546794b9c54a988d0b7282db544de913dc6c5a46a50fb", size = 8466, upload-time = "2019-10-05T20:24:03.023Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/9d/4e633743766b159d20cca63fc56c0817633d0f99c49607d72b796a386d46/bandit_sarif_formatter-1.1.1-py3-none-any.whl", hash = "sha256:2a8351cecc03b265ec7546794b9c54a988d0b7282db544de913dc6c5a46a50fb", size = 8466, upload-time = "2019-10-05T20:24:03.023Z" }, ] [[package]] name = "boltons" version = "21.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ad/1f/6c0608d86e0fc77c982a2923ece80eef85f091f2332fc13cbce41d70d502/boltons-21.0.0.tar.gz", hash = "sha256:65e70a79a731a7fe6e98592ecfb5ccf2115873d01dbc576079874629e5c90f13", size = 180201, upload-time = "2021-05-17T01:20:17.802Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ad/1f/6c0608d86e0fc77c982a2923ece80eef85f091f2332fc13cbce41d70d502/boltons-21.0.0.tar.gz", hash = "sha256:65e70a79a731a7fe6e98592ecfb5ccf2115873d01dbc576079874629e5c90f13", size = 180201, upload-time = "2021-05-17T01:20:17.802Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/a7/1a31561d10a089fcb46fe286766dd4e053a12f6e23b4fd1c26478aff2475/boltons-21.0.0-py2.py3-none-any.whl", hash = "sha256:b9bb7b58b2b420bbe11a6025fdef6d3e5edc9f76a42fb467afe7ca212ef9948b", size = 193723, upload-time = "2021-05-17T01:20:20.023Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f7/a7/1a31561d10a089fcb46fe286766dd4e053a12f6e23b4fd1c26478aff2475/boltons-21.0.0-py2.py3-none-any.whl", hash = "sha256:b9bb7b58b2b420bbe11a6025fdef6d3e5edc9f76a42fb467afe7ca212ef9948b", size = 193723, upload-time = "2021-05-17T01:20:20.023Z" }, ] [[package]] name = "boolean-py" version = "5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, ] [[package]] name = "boto3" version = "1.43.34" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/ac/178eb7f96bb6d5771105fe998b8b34512ef3f7ce9e2f1ab8d018df935bee/boto3-1.43.34.tar.gz", hash = "sha256:444207c6c883d4df3ea3b2c36df43ad492b86e0b889eebd2fc1d5ea8db0a8a1a", size = 112656, upload-time = "2026-06-19T19:33:39.366Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/01/ac/178eb7f96bb6d5771105fe998b8b34512ef3f7ce9e2f1ab8d018df935bee/boto3-1.43.34.tar.gz", hash = "sha256:444207c6c883d4df3ea3b2c36df43ad492b86e0b889eebd2fc1d5ea8db0a8a1a", size = 112656, upload-time = "2026-06-19T19:33:39.366Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5f/ac0872df61c3cea3539252f867cf8d76c226e4952f52a981b3fa54381060/boto3-1.43.34-py3-none-any.whl", hash = "sha256:42595057324606928c6e2432b3093978e4d722e0d432bce942f2a385702c0a43", size = 140029, upload-time = "2026-06-19T19:33:37.807Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4c/5f/ac0872df61c3cea3539252f867cf8d76c226e4952f52a981b3fa54381060/boto3-1.43.34-py3-none-any.whl", hash = "sha256:42595057324606928c6e2432b3093978e4d722e0d432bce942f2a385702c0a43", size = 140029, upload-time = "2026-06-19T19:33:37.807Z" }, ] [[package]] name = "botocore" version = "1.43.34" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/0d/559cdceb9f6acea6b91404970b7973e28a4434fa8a70eb1416b0af478d86/botocore-1.43.34.tar.gz", hash = "sha256:ccc973cf30c6445b30afe5760f6dc949a80f1f862cb23d9c45747f2c814ece77", size = 15591382, upload-time = "2026-06-19T19:33:28.561Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/0d/559cdceb9f6acea6b91404970b7973e28a4434fa8a70eb1416b0af478d86/botocore-1.43.34.tar.gz", hash = "sha256:ccc973cf30c6445b30afe5760f6dc949a80f1f862cb23d9c45747f2c814ece77", size = 15591382, upload-time = "2026-06-19T19:33:28.561Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/ea/dc5aab38e2b3f63380810465fab92c836e9e8bce458eba4a8a896f25e1d2/botocore-1.43.34-py3-none-any.whl", hash = "sha256:238a0269f33c5914b9343900b44767e783b3e8b6dcb6e065eac8b4495601c5df", size = 15277590, upload-time = "2026-06-19T19:33:24.562Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/ea/dc5aab38e2b3f63380810465fab92c836e9e8bce458eba4a8a896f25e1d2/botocore-1.43.34-py3-none-any.whl", hash = "sha256:238a0269f33c5914b9343900b44767e783b3e8b6dcb6e065eac8b4495601c5df", size = 15277590, upload-time = "2026-06-19T19:33:24.562Z" }, ] [[package]] name = "bracex" version = "2.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, ] [[package]] name = "build" version = "1.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, @@ -300,31 +300,31 @@ dependencies = [ { name = "pyproject-hooks" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, ] [[package]] name = "bump2version" version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/2a/688aca6eeebfe8941235be53f4da780c6edee05dbbea5d7abaa3aab6fad2/bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6", size = 36236, upload-time = "2020-10-07T18:38:40.119Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/29/2a/688aca6eeebfe8941235be53f4da780c6edee05dbbea5d7abaa3aab6fad2/bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6", size = 36236, upload-time = "2020-10-07T18:38:40.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/e3/fa60c47d7c344533142eb3af0b73234ef8ea3fb2da742ab976b947e717df/bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410", size = 22030, upload-time = "2020-10-07T18:38:38.148Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1d/e3/fa60c47d7c344533142eb3af0b73234ef8ea3fb2da742ab976b947e717df/bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410", size = 22030, upload-time = "2020-10-07T18:38:38.148Z" }, ] [[package]] name = "cachecontrol" version = "0.14.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "msgpack" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" }, ] [package.optional-dependencies] @@ -335,365 +335,365 @@ filecache = [ [[package]] name = "certifi" version = "2026.6.17" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] name = "cffi" version = "2.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { 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" }, +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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 = "cfgv" version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] name = "cfn-lint" version = "1.51.5" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "aws-sam-translator" }, { name = "jsonpatch" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" }, marker = "python_full_version >= '3.11'" }, { name = "pyyaml" }, { name = "regex" }, { name = "sympy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/69/d9e8f555ded51061f73aa2cfbe30b0b6d5273724f5563655f6dc8b77ecdd/cfn_lint-1.51.5.tar.gz", hash = "sha256:018a00f1f9eeadc196afbdc0ac8c6221c29411747c8dcff2f431d48d4080c83b", size = 4114038, upload-time = "2026-06-15T21:58:13.885Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/69/d9e8f555ded51061f73aa2cfbe30b0b6d5273724f5563655f6dc8b77ecdd/cfn_lint-1.51.5.tar.gz", hash = "sha256:018a00f1f9eeadc196afbdc0ac8c6221c29411747c8dcff2f431d48d4080c83b", size = 4114038, upload-time = "2026-06-15T21:58:13.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/18/f881634f7a03ca3914f8943ed4aa46a5f64fb121c99b983c663760931674/cfn_lint-1.51.5-py3-none-any.whl", hash = "sha256:e17c11a1485a2586c5ddf48d0041f8fa3da3038612fd7dbc559ec87935f53452", size = 6150401, upload-time = "2026-06-15T21:58:11.573Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c9/18/f881634f7a03ca3914f8943ed4aa46a5f64fb121c99b983c663760931674/cfn_lint-1.51.5-py3-none-any.whl", hash = "sha256:e17c11a1485a2586c5ddf48d0041f8fa3da3038612fd7dbc559ec87935f53452", size = 6150401, upload-time = "2026-06-15T21:58:11.573Z" }, ] [[package]] name = "chardet" version = "5.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, - { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, - { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, - { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, - { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] name = "click" version = "8.1.8" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] name = "click-option-group" version = "0.5.9" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "click" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/ff/d291d66595b30b83d1cb9e314b2c9be7cfc7327d4a0d40a15da2416ea97b/click_option_group-0.5.9.tar.gz", hash = "sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823", size = 22222, upload-time = "2025-10-09T09:38:01.474Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ef/ff/d291d66595b30b83d1cb9e314b2c9be7cfc7327d4a0d40a15da2416ea97b/click_option_group-0.5.9.tar.gz", hash = "sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823", size = 22222, upload-time = "2025-10-09T09:38:01.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/45/54bb2d8d4138964a94bef6e9afe48b0be4705ba66ac442ae7d8a8dc4ffef/click_option_group-0.5.9-py3-none-any.whl", hash = "sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080", size = 11553, upload-time = "2025-10-09T09:38:00.066Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/75/45/54bb2d8d4138964a94bef6e9afe48b0be4705ba66ac442ae7d8a8dc4ffef/click_option_group-0.5.9-py3-none-any.whl", hash = "sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080", size = 11553, upload-time = "2025-10-09T09:38:00.066Z" }, ] [[package]] name = "colorama" version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.14.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/a3/3834a5564fe8f32154cd7032400d3c2f9c565b2a373fa671f2bbdad6f634/coverage-7.14.2.tar.gz", hash = "sha256:7a2da3d81cfe17c18038c6d98e6592aa9147d596d056119b0ee612c3c8bd5230", size = 923982, upload-time = "2026-06-20T14:49:30.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/7f/551ebe25fa3de95ebbd3528b01ffd672b418e9c521b8555f85fb8aca21f8/coverage-7.14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b75818e3046e9319143157f3dc4b43679a550c2060a17cbf3e39cc0b552925", size = 220230, upload-time = "2026-06-20T14:47:09.177Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ec/a444a1a21b46e54298357977d8ab6c388e5755bc79effaf587808fdb405f/coverage-7.14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66b08ba4c5cbf0eaa2e9692b203073f198d5d469d8b15d1c7a4854ce7032b2e2", size = 220750, upload-time = "2026-06-20T14:47:11.243Z" }, - { url = "https://files.pythonhosted.org/packages/38/e5/0dce79f914e31fa0810ab770b06cc638fe5137af259649fafd4daeb2c8e5/coverage-7.14.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:70f266b536c590060b707dddfb6cf9f17e24fd30b992242e774543d256265c43", size = 247487, upload-time = "2026-06-20T14:47:12.564Z" }, - { url = "https://files.pythonhosted.org/packages/52/bb/aaca2c75ca6a5da71c3f413ac5920fe9f6e1aad387dae52a3315adf313e0/coverage-7.14.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb40cac5b1a6378fdccc99268f1033112ee4636e4fd9aaf240f6930d1fcea12c", size = 249316, upload-time = "2026-06-20T14:47:13.938Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/b45f5edd19ab9f79f7c6a4da2b4d1bd5969e0d7605fe197b60405c7129da/coverage-7.14.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c301fe9990cb5c081bf4881cb498743807c8e0e93fad7b85c02788456492ef8", size = 251182, upload-time = "2026-06-20T14:47:15.144Z" }, - { url = "https://files.pythonhosted.org/packages/f6/7c/ad5fd04da4565e7c9ad35080a5fb4762ed2d0f4893ac7eff6a1e7364e79f/coverage-7.14.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d67b0462c8a3c3d93033e7c79cacdfc57d08e5220d9115bcb24a23edf5a5900d", size = 253095, upload-time = "2026-06-20T14:47:16.468Z" }, - { url = "https://files.pythonhosted.org/packages/00/f9/41279b303e8773e1fd9a621a80159c7ed7b643dd9c7e85fd7fc3c88ebdaa/coverage-7.14.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e763087828ee9644f0c89c57f9b75f0a50fdf3e8f5d8fac5cfc351337e89a99", size = 248203, upload-time = "2026-06-20T14:47:17.758Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ad/205dddd96954fcf7c7f0b509af614c0edc2a547115ad2fb52a8fc9cbaa41/coverage-7.14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6d4da2baab6d96ceedd9176b3c142e1198b0310bc8dc04e18a3caab65c3a322c", size = 249223, upload-time = "2026-06-20T14:47:19.276Z" }, - { url = "https://files.pythonhosted.org/packages/3f/da/46c437176338ece41effbbd07d2941378db04b3e618dce68197d59a870b4/coverage-7.14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ab565a405bfdea61260145d8cc987aa66d1998fd0e0ccd4348008f4e6a39ee33", size = 247226, upload-time = "2026-06-20T14:47:20.613Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a7/42dfcb471dedb51d366762528e53f232427e8d897b92a9106b4aacec74fc/coverage-7.14.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c13230b688fbb9122251b74daa092175811eb64cb7bd1c98e2c8193dfa2b0bd5", size = 251039, upload-time = "2026-06-20T14:47:22.027Z" }, - { url = "https://files.pythonhosted.org/packages/3c/29/987df1a9f8843d3b1f50cb47cca0b10c983e84a3b1b9f6965796f07efa4e/coverage-7.14.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:014c83ba1ec97993cfe94e77fe6b56daa76bc0c218b86938971574c28942d044", size = 247497, upload-time = "2026-06-20T14:47:23.374Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1c/919b6624c35161d183c43f57fca52ebcc5a59a7b3fa52fe0d0c3067469f5/coverage-7.14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6caf54ffbf84b30470a8118f275afee9234e616572e4e41bae1dc19198c37294", size = 248100, upload-time = "2026-06-20T14:47:24.621Z" }, - { url = "https://files.pythonhosted.org/packages/6f/15/acbc7b5a6184f92d4875b820477f0bbb3a87371408f5ef73a575bdd3b8e1/coverage-7.14.2-cp310-cp310-win32.whl", hash = "sha256:4bf9d8a35f77df5638c61b5012ba5225109ec1cc15bc5eb097036b3c3cc939f3", size = 222282, upload-time = "2026-06-20T14:47:25.893Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3f/944e24fdb2e88549b58bfd5a51a3a66481cf21154c7aa1a494597c870125/coverage-7.14.2-cp310-cp310-win_amd64.whl", hash = "sha256:c1f17a8caebe0facd4556b1e0adfe0987c17feebed88e7bb6b5365c45c84c5d6", size = 222910, upload-time = "2026-06-20T14:47:27.331Z" }, - { url = "https://files.pythonhosted.org/packages/04/d5/d0e511247f84fa88ae7da68403cbd3bf9d2a5fc48f5d6618a6846b275632/coverage-7.14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909f265c8c41f04c824bf741b2601fdcb56cab4bf56e018996b6494192ba0f58", size = 220352, upload-time = "2026-06-20T14:47:28.61Z" }, - { url = "https://files.pythonhosted.org/packages/03/4a/ecaff6db72e6c1782ca51336e391393f1e9cc6e4412d6c3da8b7d5075adf/coverage-7.14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8102deaf911938233f760426e6a5e287388521de95111d5c8de26c8a1028924", size = 220855, upload-time = "2026-06-20T14:47:29.972Z" }, - { url = "https://files.pythonhosted.org/packages/34/9a/cf950cd8e8df06ee5941276e69f81647005360421be523d5ca18f658e143/coverage-7.14.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:851f49e7bd7d1cdaf328f3133942b252d5e3d3380690131f423cba8e435b87f5", size = 251276, upload-time = "2026-06-20T14:47:31.413Z" }, - { url = "https://files.pythonhosted.org/packages/9d/08/f973be32c9a095e4bb2d3a7bdcb2f9c117e39d4062471ffffae3623f6c51/coverage-7.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04cb445bed86aaf00aaa97d41a8b6e30f100f21e81c34caaec4efc684cb57768", size = 253189, upload-time = "2026-06-20T14:47:32.727Z" }, - { url = "https://files.pythonhosted.org/packages/96/aa/f3a50952ba553d442d94b793e5dede25d426b02e5e011e9a9dd225c002d3/coverage-7.14.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7471bc920d97c51c37ea8127f13b2adca43c3d78c53313b26a1f428e99d2c254", size = 255299, upload-time = "2026-06-20T14:47:34.019Z" }, - { url = "https://files.pythonhosted.org/packages/e0/29/9a4c491986f4d637ed64961ae56721661fc21b6b767d280848d0c708756a/coverage-7.14.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da5057e1bb257c967feee8ba67f3ebf379e801c7717f238b3d8c9caf00fc8f93", size = 257255, upload-time = "2026-06-20T14:47:35.397Z" }, - { url = "https://files.pythonhosted.org/packages/dd/61/d2a5b48007f6a212f321c36cf5486feb80505d2d00dfb1163aad2da71197/coverage-7.14.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33c0da852e8a40246cd8e20cf3b2fc17ca52a45e9b5f7983c93db26f5d24b87b", size = 251417, upload-time = "2026-06-20T14:47:36.677Z" }, - { url = "https://files.pythonhosted.org/packages/ea/25/8df66ae25b401d4529e1d0617af20d9695d171ea4ffec4ca9dffc5dc37b7/coverage-7.14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f48a85bb437fab7782021c40bfee6b15146928b96960d008ace41b6901a0f21d", size = 252991, upload-time = "2026-06-20T14:47:38.027Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/16bdc9116dd8bf412a421a7227daa65ad9f12bef0685b13c1bd1c12e6d4c/coverage-7.14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f44e7579a769a21d5b5e3166916bfe30ee175aaffff750324cbb11be2dbec5ad", size = 251051, upload-time = "2026-06-20T14:47:39.26Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f8/b7dbed84274dcc69ddb9c0fe72ec1260830473e0d6c299dcf087a0567f7c/coverage-7.14.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:78853ca3c6ca2f012daa2b07dbabbb8db0f09d4dbe8ee828d294b3445d3f4cd8", size = 254817, upload-time = "2026-06-20T14:47:40.995Z" }, - { url = "https://files.pythonhosted.org/packages/c6/07/4659e6bed01a25a0effb4952e8e75fd157038fe5f2829b0f69c6811c2033/coverage-7.14.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c9c2795ee3692097ff226ab806005d36bb9691fca9b35353542b57ea749cc830", size = 250772, upload-time = "2026-06-20T14:47:42.306Z" }, - { url = "https://files.pythonhosted.org/packages/26/f4/45019da4cd6cd1df3042476447449d62a76a201f6b3556aa40ac31bce20b/coverage-7.14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2f5cc48a845d755b6db236f8c29c2b54773eb4c7e4ee2ead43812d73718784b0", size = 251679, upload-time = "2026-06-20T14:47:43.703Z" }, - { url = "https://files.pythonhosted.org/packages/92/e5/76d75fa2ffe0285d3f2608d1bb241fc245cf98fe614d52118427dd6ccdaa/coverage-7.14.2-cp311-cp311-win32.whl", hash = "sha256:9c61cb7eaabcfa609c5bc0f5ff5869d72a2f02f17994e5fba5f971de516f3c82", size = 222445, upload-time = "2026-06-20T14:47:45.137Z" }, - { url = "https://files.pythonhosted.org/packages/57/59/696c64547e5c8b9ed31532e9c7a5f9b6474054da93f8ab07f8baf7365c57/coverage-7.14.2-cp311-cp311-win_amd64.whl", hash = "sha256:e715909b0966d1774d8a26e14e2f4a3ae75909dca526901c6306286b2dcbfbdc", size = 222922, upload-time = "2026-06-20T14:47:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/63/72/646a28100462996c11b98e27d6786cd61f48100d1479804846a3e1e5bf9b/coverage-7.14.2-cp311-cp311-win_arm64.whl", hash = "sha256:9193f7150937a4fd836b10eaa123e15d98e961d1fabac07e60adf2d4785f888a", size = 222468, upload-time = "2026-06-20T14:47:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/bdd141aa2c605096a8ef63b8435fd4f5fec78946a3cb7b9145840ec78291/coverage-7.14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:37c94712e533ea06f0b1e4d934811c520b1914ce0e4da3916220717aa7a86bc6", size = 220528, upload-time = "2026-06-20T14:47:49.652Z" }, - { url = "https://files.pythonhosted.org/packages/02/97/d24ae7d2afc62c54a36313d4dedb655c9afbba3003f0f7f1ae81e97af31f/coverage-7.14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c050bbc7bba94c77e4ed7438f4fda1babe98ab145691d80aa6f60df934a1468b", size = 220883, upload-time = "2026-06-20T14:47:51.036Z" }, - { url = "https://files.pythonhosted.org/packages/f8/0e/d8f00efd3df0d63e6843ebcbade9e4119d60f5376753c9705d84b014c775/coverage-7.14.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a7af571767a2ee342a171c16fc1b1a07a0bf511606d381703fb7cf397fe49d46", size = 252395, upload-time = "2026-06-20T14:47:52.627Z" }, - { url = "https://files.pythonhosted.org/packages/1c/1c/ab9510dfe1a16a35a10f90efad0d9a9cf61b9876973752968f2ba882f73f/coverage-7.14.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8b4910cce599cd2438f8da65f5ef199a70a1cdb6ab314926df78271ca5954240", size = 255131, upload-time = "2026-06-20T14:47:54.235Z" }, - { url = "https://files.pythonhosted.org/packages/ba/dd/70171e9371003b33dc6b20f527ac216ff91bbe5c1088e754eb8950d79193/coverage-7.14.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c33e9e4878972f430b0cc06de3bf2a28d054a9efb4f8426d27de0d9cb81396ff", size = 256246, upload-time = "2026-06-20T14:47:55.61Z" }, - { url = "https://files.pythonhosted.org/packages/0f/80/a68b1dd81d5c011e17fd6ab0d707d33297df1d0c618114b9b750a2219c80/coverage-7.14.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7967ea55c6dea6becba4d5870e2fa0aa4915a8be7ebff1bb79e6207aa75ce8d", size = 258504, upload-time = "2026-06-20T14:47:56.979Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7b/40baaa946189f5317cd77d484e39b9b0727d02ebada0a12162374f2faee2/coverage-7.14.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1322f237c2979b84096f4239c17828ff17fea6b3bbe96c44381c5f587c44c26", size = 252808, upload-time = "2026-06-20T14:47:58.418Z" }, - { url = "https://files.pythonhosted.org/packages/d5/05/b19517b09c43d1e8591de6c13178b0c03166c31e1adbebda378e64c66b9a/coverage-7.14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:77849525340c99f516d793dddbcee16b18d50af892ac43c8de1a6f343d41e3b5", size = 254166, upload-time = "2026-06-20T14:48:00.004Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f5/6e65da5957e041d2094a9b97736628dd80160f1cc007a50790bbb2668c1a/coverage-7.14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef11695493ec3f06f7b2678ca274bcabb4ca04057317df268ddbfd8b05f661a8", size = 252310, upload-time = "2026-06-20T14:48:01.458Z" }, - { url = "https://files.pythonhosted.org/packages/2d/de/01b5274f0db63175b04d9354eff68d2d268b8b57a1b2db7d3dcb1f2c9dbb/coverage-7.14.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8134f0e0723e080d1c27bbe8fc149f0162e429fa1852482150015d0fce83eaf1", size = 256379, upload-time = "2026-06-20T14:48:02.981Z" }, - { url = "https://files.pythonhosted.org/packages/71/d6/9a2ffbca41e2f8f86f61e8b78b86afa433ec8cdeac4908ace93a28fe3ff0/coverage-7.14.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:914eead2b843fc357f733b3fe39cc94f1b53d466e8cfe03080b1ed9d24ccfc73", size = 251880, upload-time = "2026-06-20T14:48:04.463Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ff/20bd54a43c88c08f474e6cb355a97e024e38412873ef0a581629abe1e26f/coverage-7.14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4b2d5e847fb7958583b74910cc19e5ec4ece514487385677b26433b2546116e", size = 253753, upload-time = "2026-06-20T14:48:05.99Z" }, - { url = "https://files.pythonhosted.org/packages/35/2a/2b3482c30d8344f301d8df6ff232a321f2ab87d5ac97ba21891a68638131/coverage-7.14.2-cp312-cp312-win32.whl", hash = "sha256:e753db9e40dda7302e0ac3e1e6e1325fb7f7b4694f87a7314ab15dd5d57911a7", size = 222584, upload-time = "2026-06-20T14:48:07.361Z" }, - { url = "https://files.pythonhosted.org/packages/f6/5e/83934ffff147edd313fe925db426e8f7ccad9e4663262eb5c4db4e345658/coverage-7.14.2-cp312-cp312-win_amd64.whl", hash = "sha256:d32e5ca5f16dafb269ee50b60d32b00c704b3f6f78e238105f1d94a3a5f24bf5", size = 223118, upload-time = "2026-06-20T14:48:08.837Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ee/616b4f38a34f076f3045d3eedfa764d34d82e6a6cc6b300acb0f1ff22a98/coverage-7.14.2-cp312-cp312-win_arm64.whl", hash = "sha256:dc366f158e2fb2add9d4e57338ca48f12611024278688ee657eb0b853fcb5de5", size = 222504, upload-time = "2026-06-20T14:48:10.436Z" }, - { url = "https://files.pythonhosted.org/packages/6d/09/b5b334c27960e7aac0003b96491bada7838dc641099fa64a1a598abf33cd/coverage-7.14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e5f077641a6713ce9d38df9e85d4fb9e008677fc0775cbaeb32ddfc3b319d4ca", size = 220552, upload-time = "2026-06-20T14:48:11.847Z" }, - { url = "https://files.pythonhosted.org/packages/79/20/879a000c319b4df7b50e4d688c0f7c0f6b5ac9d7b18848cbc00eabf26efe/coverage-7.14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0907f39b49ae818fe8af50aaa0f19afbc8ca164aea0865181ca7af17a3ac690b", size = 220919, upload-time = "2026-06-20T14:48:13.397Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b7/326dded4371bab60f42215797944a356e4d81a3cee106121c7f7dd531604/coverage-7.14.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734d47669118d75c28981e562d4530ceb77342d31ffef6def5edd5ad4f05d7b", size = 251917, upload-time = "2026-06-20T14:48:14.931Z" }, - { url = "https://files.pythonhosted.org/packages/eb/14/b3232ba218a0d1a70883d2675f18ff465de9e8e5e3346e81dc2b079838bd/coverage-7.14.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d9a1b5813d00ea6151f6ccf64d1fa16892771dfdda12ba87162d15ec4ea3e1e", size = 254515, upload-time = "2026-06-20T14:48:16.545Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/d77bcbee1cad71b42776574114b462225cc9125b4982f43da1b66adc850f/coverage-7.14.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f0a80f4c8ac3f774210b1cc1bc0e31e75502f2818dda9a144ff90e702c4d91d", size = 255749, upload-time = "2026-06-20T14:48:18.214Z" }, - { url = "https://files.pythonhosted.org/packages/86/86/97377937b29e9e44a1529bb20cb74dbcf80ed9006d87d7e742ff69e44b67/coverage-7.14.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e66f3f22d6c1515ce70f2e7c3e9c6f3ff0ff33480125c9f9c53e8f6508e30f", size = 257882, upload-time = "2026-06-20T14:48:19.7Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a4/0fc8fe68bc505450bb068a2823ac7797bd8495240ccb8b4a5a1da1ee7e62/coverage-7.14.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6a2c37c3114f87ca7f10113756026eecb49656514debad600dcbec21f355ccea", size = 252144, upload-time = "2026-06-20T14:48:21.176Z" }, - { url = "https://files.pythonhosted.org/packages/8d/4a/450094ddc41ab0d2eb4a0457b3856400ea3329568d1303696e85de099ae6/coverage-7.14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b16a7959d04b1497281c062c180413565c3f3469211d78799ad5b9a75f67796", size = 253882, upload-time = "2026-06-20T14:48:22.701Z" }, - { url = "https://files.pythonhosted.org/packages/d0/28/2f6ae6d98265d9aa6bac311c4a93403675905b03aca95dc4373080279d75/coverage-7.14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6466c6999545cf00c4c142dfcbbf2db396dc735f005dcf8f91d57e351a79472b", size = 251846, upload-time = "2026-06-20T14:48:24.295Z" }, - { url = "https://files.pythonhosted.org/packages/c2/6e/707281468400794d52874e8fb5e38ff7578a0ff32ed49fe4fe85f192d0fc/coverage-7.14.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c60915ebb8f562317ba5ff6b8c32e25c0882289b201a9f2fb2987f91efd95d8", size = 256002, upload-time = "2026-06-20T14:48:26.015Z" }, - { url = "https://files.pythonhosted.org/packages/c2/83/5e963120de4011257a950ce4cfb7fc833ddf3fee19db495268d3dec28154/coverage-7.14.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:33b830850488acbcd358c78a4fecfafe7031667b4da8ddff5546295dc962cdeb", size = 251665, upload-time = "2026-06-20T14:48:27.654Z" }, - { url = "https://files.pythonhosted.org/packages/e9/78/66b482cd525083bcc0bc894c16db79dabac37490065b53b07d6e8ab77202/coverage-7.14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d0f845539230b8269aec902bc978b0cc403f52f002d18a04492efc943404d0bc", size = 253435, upload-time = "2026-06-20T14:48:29.354Z" }, - { url = "https://files.pythonhosted.org/packages/e6/61/0663fb8cb530c8b11819b920109694eee95a3b22960a9495be0200f657f1/coverage-7.14.2-cp313-cp313-win32.whl", hash = "sha256:a8ac51a2e441e9119b9395f4d893fbc4934c64c8ba58be9b9eaa85591249e548", size = 222591, upload-time = "2026-06-20T14:48:31.142Z" }, - { url = "https://files.pythonhosted.org/packages/a6/47/1536d2b009c2848c3682500f497053f4645e70911afe02f594000997831a/coverage-7.14.2-cp313-cp313-win_amd64.whl", hash = "sha256:039b264cdb31c44b48f9821e2afbf8f37df49e0fb837e24a942918b36c567e31", size = 223134, upload-time = "2026-06-20T14:48:32.696Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/33ba4f335dd60bb34350318283d784f46018070e67b7d4df7c910ec9d9a0/coverage-7.14.2-cp313-cp313-win_arm64.whl", hash = "sha256:7f2ef591e381cc36b8e53334e1b842c760c520c8a52d01e8626209400e93fe6a", size = 222529, upload-time = "2026-06-20T14:48:34.237Z" }, - { url = "https://files.pythonhosted.org/packages/fc/bc/120390669817ede714ab141ae0a2a73240fd7354aac992c41dc0bd19570f/coverage-7.14.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7a0d1f026b72d627fa5c8a57cbc86ad209b64aa2a65833c83b290ace5cbee126", size = 220593, upload-time = "2026-06-20T14:48:35.755Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a3/7f1cfacd76af91e585f7ad689d7168002b444ed2a8ce59f2daaff10089b5/coverage-7.14.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4d2b86f81c1c9310a7e774e3cc9e927a3d0bf583ecbfa01498dd626930025428", size = 220925, upload-time = "2026-06-20T14:48:37.35Z" }, - { url = "https://files.pythonhosted.org/packages/e7/10/6514b2525bb672eb8b43703e46d061d694111db21efe7609db722df2233f/coverage-7.14.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d76bdc1f9396ae70a55d050cf9743d88141c62ce0a22a3f627fab1d11c2f8bc6", size = 251974, upload-time = "2026-06-20T14:48:39.109Z" }, - { url = "https://files.pythonhosted.org/packages/23/b4/4533091541c6620ecd68115bbfa1c61265b775618adef3a5fd137f4582e9/coverage-7.14.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cda36d8e7bfd63b3e44e75163265429caa5d935b672b00f71bccc8c010518c64", size = 254479, upload-time = "2026-06-20T14:48:40.871Z" }, - { url = "https://files.pythonhosted.org/packages/06/af/e251a143d5d106385dbca696c553afab6b69f7f6bc376a34e089cc0b8b32/coverage-7.14.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0904f3b79d7b845bef0715afe1900da634d12b97f05b9479cb472880ca07cb9c", size = 255824, upload-time = "2026-06-20T14:48:42.608Z" }, - { url = "https://files.pythonhosted.org/packages/9c/53/9e5876e60efbaa79d743d1948a5015ddc05b808db1cd62228acf83e87d43/coverage-7.14.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6795ca4198d6cb7fc2c6163214f6555a6bc5f0ae1e268e76139dec4b37c4499", size = 258139, upload-time = "2026-06-20T14:48:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/85/5a/d35a4f431fb594e46b81cad4a13b470b017e918f347c1c0b260f7494fa1e/coverage-7.14.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c41e9b60fc0fa57f5d73306417d2f9d668202cca6944f9435878c55a5e7ae213", size = 252002, upload-time = "2026-06-20T14:48:45.961Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e2/f5b304c8139c606c4f1b230d3a257d0c88edfbbdf06c58364f07625dc45c/coverage-7.14.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419d2aadd5746efc2e9df0f33c05570d8192e6f6a6098ab05acce586f44ce8a5", size = 253832, upload-time = "2026-06-20T14:48:47.582Z" }, - { url = "https://files.pythonhosted.org/packages/86/bc/bbbd283daa6be4f68aad4ad4066fd39ae98e4174db8c03ab26c5803d6234/coverage-7.14.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c5d273c5f1411c0d26c4f066c398d4a434b1f97bb5fa409189bedce86d4add4", size = 251799, upload-time = "2026-06-20T14:48:49.42Z" }, - { url = "https://files.pythonhosted.org/packages/69/8d/0745fceb89c9e5f7dd8ed243d97dc8561b7a95545741e2409d2b34654824/coverage-7.14.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5fe465bc691264adce601527a972990c1174075d86bcbe9968fd20c95e0b1948", size = 256075, upload-time = "2026-06-20T14:48:51.065Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a0/441d9a5255cf021ab41ee00c014a4607d1c72d5e5bef0a4fdaa5be86a907/coverage-7.14.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6fbb61617af1c56f95d53170ae9fa6c9aef6de1abd02fcc50064bfc672efb18d", size = 251612, upload-time = "2026-06-20T14:48:52.653Z" }, - { url = "https://files.pythonhosted.org/packages/50/37/3d19c5e32d4a529c068eb296abfa3e455bd2c0f9311ecf26280f408ff8e0/coverage-7.14.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e1eff22b831dfd5694989cc1f0789980f18391f614ac67c851af9a8e6d25e9ba", size = 253270, upload-time = "2026-06-20T14:48:54.3Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b0/54dd13937297518da6d092cc2c39d9340ec2194bdfa92e0a64694d643e23/coverage-7.14.2-cp314-cp314-win32.whl", hash = "sha256:58e91be0a233adef698d3e6be54f10401bb91fd7854c0d4c4d50e0d3711e72f1", size = 222796, upload-time = "2026-06-20T14:48:56.084Z" }, - { url = "https://files.pythonhosted.org/packages/51/45/7a10e0909919686e335fdd95869cfb222d55243ebff27dc5cf59ca259a1f/coverage-7.14.2-cp314-cp314-win_amd64.whl", hash = "sha256:d8429bf97906bfe6c61f9dbfb3342e0d88120da61939da8bd04f830cc3eab3b8", size = 223285, upload-time = "2026-06-20T14:48:57.729Z" }, - { url = "https://files.pythonhosted.org/packages/2e/03/9cb197eb4b3d1a2eccb2537c226a93c80522c5b8afc5dd93e1993d7bb021/coverage-7.14.2-cp314-cp314-win_arm64.whl", hash = "sha256:13609d9d77249447aa73357b14831b0f3b95f275026c9ff20dd105f981f53a0c", size = 222712, upload-time = "2026-06-20T14:48:59.413Z" }, - { url = "https://files.pythonhosted.org/packages/d6/3c/e59f498511080d20bf866b0af9eeab820feb91547dae2084cb9bb7fb0e58/coverage-7.14.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9818486c2bac88ae931df7e04905ee29bef49fd218c00f5f02bed4855254a101", size = 221325, upload-time = "2026-06-20T14:49:01.447Z" }, - { url = "https://files.pythonhosted.org/packages/d3/37/8d7955f7e701e69198bd0a0132ea76518c078a635b930a4924e2ccfa70f0/coverage-7.14.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:58055adffabfa243516a197aa9f85f0dd56d905b0fba1a10193269759c29ccb0", size = 221594, upload-time = "2026-06-20T14:49:03.13Z" }, - { url = "https://files.pythonhosted.org/packages/34/7a/6738e1e1533ce8ec4e2e472696eefdd4723864d7efaa140e433053bf576a/coverage-7.14.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:535747dbc200349d7fb434cffcb28e770f0290f69b225f56dc3803aa7210cdea", size = 262957, upload-time = "2026-06-20T14:49:04.829Z" }, - { url = "https://files.pythonhosted.org/packages/35/c4/d1be863cd39e0955904315fece67c5c23e046563f5eea0ceac16c547a759/coverage-7.14.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:420c66e35d85c0ca5dc6a38147d83ef239762542900e5921ebbdb89333c540ea", size = 265081, upload-time = "2026-06-20T14:49:07.018Z" }, - { url = "https://files.pythonhosted.org/packages/72/7f/412df3c3c251284a11834287fd6f7e3bb98c528c53e030589e9344a3ef80/coverage-7.14.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2cf17b33773be446a588551ea6a746b2d70dd0bc90dc31f1dd7648975a63c6b", size = 267500, upload-time = "2026-06-20T14:49:08.709Z" }, - { url = "https://files.pythonhosted.org/packages/54/68/7d0764e83459455384d5c04179ce2d2a837bef01b9ba463079c6e8b31361/coverage-7.14.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:adb4a5fef041f7179bb264203add873c147d169cf2f8d0adae89ff2e51271bac", size = 268619, upload-time = "2026-06-20T14:49:10.405Z" }, - { url = "https://files.pythonhosted.org/packages/14/68/1292164ac70cbcc86ac3982da31a6fbb42bb4bcebf6e5cf73c99cfcfd50d/coverage-7.14.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c012ec357dec9408a83dad5541172a63c5cfa1421709f2e5811480d31ae1b28", size = 262066, upload-time = "2026-06-20T14:49:12.257Z" }, - { url = "https://files.pythonhosted.org/packages/20/44/fd6fdf3f63b6e00a1a9230022d072ded5189576001685706aa6524187c65/coverage-7.14.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dacd0ecd08fda3cb2f85b60cabea7da326dcb2fc15fbb23a88830a80144cc9f2", size = 264953, upload-time = "2026-06-20T14:49:14.13Z" }, - { url = "https://files.pythonhosted.org/packages/39/29/e803fea3da89eaeb5b6b41b3ccd039fe9f3300a900e3803baac1a998529f/coverage-7.14.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f27e980f2feba5dfe7a32b22b125470de69c0bd113c75e16165de909a777f512", size = 262555, upload-time = "2026-06-20T14:49:15.803Z" }, - { url = "https://files.pythonhosted.org/packages/32/3c/b360e48ac68e3236c04cb83658382e7f5be7efbbec2e1faae3dcca432783/coverage-7.14.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:105c00efb65c863630b2b63cbf7b8267e4da2d44b62284efbb19a03b04c337d4", size = 266289, upload-time = "2026-06-20T14:49:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/59/12/1ed6d9274d599c586e2d1aa9818765dcdae6bb52aa88afa2fcd868398191/coverage-7.14.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:571173fa04c8e8d6235ab32ae67fecca97777e2e1b4a1a30f3022c34e397c1c1", size = 261402, upload-time = "2026-06-20T14:49:19.708Z" }, - { url = "https://files.pythonhosted.org/packages/44/17/eb6cf12a4538cda937aefbeabb15377a8a30b377b484e63d31c9da790966/coverage-7.14.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e532f34d42d1a421fa00ed6b7735d14ac2e340256c1bad26a5e1dc1252b0bed7", size = 263715, upload-time = "2026-06-20T14:49:21.427Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ca/4bafdb9d372ab05d6ed3a63e7f00d3195d169d0afea00f617c026e386c19/coverage-7.14.2-cp314-cp314t-win32.whl", hash = "sha256:243971550fb46c3039257f75e65610002d84304c505f609bbd9779e20a653a0a", size = 223103, upload-time = "2026-06-20T14:49:23.24Z" }, - { url = "https://files.pythonhosted.org/packages/35/cb/0765dbd9011d2e47315f1da31e62c5fe231f04a6ec8da213e64c4505896d/coverage-7.14.2-cp314-cp314t-win_amd64.whl", hash = "sha256:60fb0ca084a92da96474b8b405a7ea76dfecac3c68db54383e7934b6f3871169", size = 223934, upload-time = "2026-06-20T14:49:25.347Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ce/373dde027ecd0ae54511430fe7569f838d3a0376b70333ba9fd20c76b836/coverage-7.14.2-cp314-cp314t-win_arm64.whl", hash = "sha256:36a0a3f42ed7dfdbca2a69a541519ffd5064a5692152fc0018109e74370d7345", size = 223249, upload-time = "2026-06-20T14:49:27.241Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5e/a8ba14ceb014f39bd5e3f7077150718c7de61c01ce326bfe7e8eae9b19b2/coverage-7.14.2-py3-none-any.whl", hash = "sha256:04d92589e481a8b68a005a5a1e0646a91c76f322c397c4635298c57cf63699b5", size = 212325, upload-time = "2026-06-20T14:49:28.991Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9c/a3/3834a5564fe8f32154cd7032400d3c2f9c565b2a373fa671f2bbdad6f634/coverage-7.14.2.tar.gz", hash = "sha256:7a2da3d81cfe17c18038c6d98e6592aa9147d596d056119b0ee612c3c8bd5230", size = 923982, upload-time = "2026-06-20T14:49:30.885Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/56/7f/551ebe25fa3de95ebbd3528b01ffd672b418e9c521b8555f85fb8aca21f8/coverage-7.14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b75818e3046e9319143157f3dc4b43679a550c2060a17cbf3e39cc0b552925", size = 220230, upload-time = "2026-06-20T14:47:09.177Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/ec/a444a1a21b46e54298357977d8ab6c388e5755bc79effaf587808fdb405f/coverage-7.14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66b08ba4c5cbf0eaa2e9692b203073f198d5d469d8b15d1c7a4854ce7032b2e2", size = 220750, upload-time = "2026-06-20T14:47:11.243Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/38/e5/0dce79f914e31fa0810ab770b06cc638fe5137af259649fafd4daeb2c8e5/coverage-7.14.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:70f266b536c590060b707dddfb6cf9f17e24fd30b992242e774543d256265c43", size = 247487, upload-time = "2026-06-20T14:47:12.564Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/52/bb/aaca2c75ca6a5da71c3f413ac5920fe9f6e1aad387dae52a3315adf313e0/coverage-7.14.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb40cac5b1a6378fdccc99268f1033112ee4636e4fd9aaf240f6930d1fcea12c", size = 249316, upload-time = "2026-06-20T14:47:13.938Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/37/b3/b45f5edd19ab9f79f7c6a4da2b4d1bd5969e0d7605fe197b60405c7129da/coverage-7.14.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c301fe9990cb5c081bf4881cb498743807c8e0e93fad7b85c02788456492ef8", size = 251182, upload-time = "2026-06-20T14:47:15.144Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f6/7c/ad5fd04da4565e7c9ad35080a5fb4762ed2d0f4893ac7eff6a1e7364e79f/coverage-7.14.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d67b0462c8a3c3d93033e7c79cacdfc57d08e5220d9115bcb24a23edf5a5900d", size = 253095, upload-time = "2026-06-20T14:47:16.468Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/00/f9/41279b303e8773e1fd9a621a80159c7ed7b643dd9c7e85fd7fc3c88ebdaa/coverage-7.14.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e763087828ee9644f0c89c57f9b75f0a50fdf3e8f5d8fac5cfc351337e89a99", size = 248203, upload-time = "2026-06-20T14:47:17.758Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6c/ad/205dddd96954fcf7c7f0b509af614c0edc2a547115ad2fb52a8fc9cbaa41/coverage-7.14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6d4da2baab6d96ceedd9176b3c142e1198b0310bc8dc04e18a3caab65c3a322c", size = 249223, upload-time = "2026-06-20T14:47:19.276Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3f/da/46c437176338ece41effbbd07d2941378db04b3e618dce68197d59a870b4/coverage-7.14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ab565a405bfdea61260145d8cc987aa66d1998fd0e0ccd4348008f4e6a39ee33", size = 247226, upload-time = "2026-06-20T14:47:20.613Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/a7/42dfcb471dedb51d366762528e53f232427e8d897b92a9106b4aacec74fc/coverage-7.14.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c13230b688fbb9122251b74daa092175811eb64cb7bd1c98e2c8193dfa2b0bd5", size = 251039, upload-time = "2026-06-20T14:47:22.027Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/29/987df1a9f8843d3b1f50cb47cca0b10c983e84a3b1b9f6965796f07efa4e/coverage-7.14.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:014c83ba1ec97993cfe94e77fe6b56daa76bc0c218b86938971574c28942d044", size = 247497, upload-time = "2026-06-20T14:47:23.374Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8f/1c/919b6624c35161d183c43f57fca52ebcc5a59a7b3fa52fe0d0c3067469f5/coverage-7.14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6caf54ffbf84b30470a8118f275afee9234e616572e4e41bae1dc19198c37294", size = 248100, upload-time = "2026-06-20T14:47:24.621Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6f/15/acbc7b5a6184f92d4875b820477f0bbb3a87371408f5ef73a575bdd3b8e1/coverage-7.14.2-cp310-cp310-win32.whl", hash = "sha256:4bf9d8a35f77df5638c61b5012ba5225109ec1cc15bc5eb097036b3c3cc939f3", size = 222282, upload-time = "2026-06-20T14:47:25.893Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2f/3f/944e24fdb2e88549b58bfd5a51a3a66481cf21154c7aa1a494597c870125/coverage-7.14.2-cp310-cp310-win_amd64.whl", hash = "sha256:c1f17a8caebe0facd4556b1e0adfe0987c17feebed88e7bb6b5365c45c84c5d6", size = 222910, upload-time = "2026-06-20T14:47:27.331Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/04/d5/d0e511247f84fa88ae7da68403cbd3bf9d2a5fc48f5d6618a6846b275632/coverage-7.14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909f265c8c41f04c824bf741b2601fdcb56cab4bf56e018996b6494192ba0f58", size = 220352, upload-time = "2026-06-20T14:47:28.61Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/03/4a/ecaff6db72e6c1782ca51336e391393f1e9cc6e4412d6c3da8b7d5075adf/coverage-7.14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8102deaf911938233f760426e6a5e287388521de95111d5c8de26c8a1028924", size = 220855, upload-time = "2026-06-20T14:47:29.972Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/34/9a/cf950cd8e8df06ee5941276e69f81647005360421be523d5ca18f658e143/coverage-7.14.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:851f49e7bd7d1cdaf328f3133942b252d5e3d3380690131f423cba8e435b87f5", size = 251276, upload-time = "2026-06-20T14:47:31.413Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9d/08/f973be32c9a095e4bb2d3a7bdcb2f9c117e39d4062471ffffae3623f6c51/coverage-7.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04cb445bed86aaf00aaa97d41a8b6e30f100f21e81c34caaec4efc684cb57768", size = 253189, upload-time = "2026-06-20T14:47:32.727Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/96/aa/f3a50952ba553d442d94b793e5dede25d426b02e5e011e9a9dd225c002d3/coverage-7.14.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7471bc920d97c51c37ea8127f13b2adca43c3d78c53313b26a1f428e99d2c254", size = 255299, upload-time = "2026-06-20T14:47:34.019Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/29/9a4c491986f4d637ed64961ae56721661fc21b6b767d280848d0c708756a/coverage-7.14.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da5057e1bb257c967feee8ba67f3ebf379e801c7717f238b3d8c9caf00fc8f93", size = 257255, upload-time = "2026-06-20T14:47:35.397Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dd/61/d2a5b48007f6a212f321c36cf5486feb80505d2d00dfb1163aad2da71197/coverage-7.14.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33c0da852e8a40246cd8e20cf3b2fc17ca52a45e9b5f7983c93db26f5d24b87b", size = 251417, upload-time = "2026-06-20T14:47:36.677Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ea/25/8df66ae25b401d4529e1d0617af20d9695d171ea4ffec4ca9dffc5dc37b7/coverage-7.14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f48a85bb437fab7782021c40bfee6b15146928b96960d008ace41b6901a0f21d", size = 252991, upload-time = "2026-06-20T14:47:38.027Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/7b/16bdc9116dd8bf412a421a7227daa65ad9f12bef0685b13c1bd1c12e6d4c/coverage-7.14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f44e7579a769a21d5b5e3166916bfe30ee175aaffff750324cbb11be2dbec5ad", size = 251051, upload-time = "2026-06-20T14:47:39.26Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0a/f8/b7dbed84274dcc69ddb9c0fe72ec1260830473e0d6c299dcf087a0567f7c/coverage-7.14.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:78853ca3c6ca2f012daa2b07dbabbb8db0f09d4dbe8ee828d294b3445d3f4cd8", size = 254817, upload-time = "2026-06-20T14:47:40.995Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c6/07/4659e6bed01a25a0effb4952e8e75fd157038fe5f2829b0f69c6811c2033/coverage-7.14.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c9c2795ee3692097ff226ab806005d36bb9691fca9b35353542b57ea749cc830", size = 250772, upload-time = "2026-06-20T14:47:42.306Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/26/f4/45019da4cd6cd1df3042476447449d62a76a201f6b3556aa40ac31bce20b/coverage-7.14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2f5cc48a845d755b6db236f8c29c2b54773eb4c7e4ee2ead43812d73718784b0", size = 251679, upload-time = "2026-06-20T14:47:43.703Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/92/e5/76d75fa2ffe0285d3f2608d1bb241fc245cf98fe614d52118427dd6ccdaa/coverage-7.14.2-cp311-cp311-win32.whl", hash = "sha256:9c61cb7eaabcfa609c5bc0f5ff5869d72a2f02f17994e5fba5f971de516f3c82", size = 222445, upload-time = "2026-06-20T14:47:45.137Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/59/696c64547e5c8b9ed31532e9c7a5f9b6474054da93f8ab07f8baf7365c57/coverage-7.14.2-cp311-cp311-win_amd64.whl", hash = "sha256:e715909b0966d1774d8a26e14e2f4a3ae75909dca526901c6306286b2dcbfbdc", size = 222922, upload-time = "2026-06-20T14:47:46.67Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/72/646a28100462996c11b98e27d6786cd61f48100d1479804846a3e1e5bf9b/coverage-7.14.2-cp311-cp311-win_arm64.whl", hash = "sha256:9193f7150937a4fd836b10eaa123e15d98e961d1fabac07e60adf2d4785f888a", size = 222468, upload-time = "2026-06-20T14:47:48.119Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/d9/bdd141aa2c605096a8ef63b8435fd4f5fec78946a3cb7b9145840ec78291/coverage-7.14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:37c94712e533ea06f0b1e4d934811c520b1914ce0e4da3916220717aa7a86bc6", size = 220528, upload-time = "2026-06-20T14:47:49.652Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/97/d24ae7d2afc62c54a36313d4dedb655c9afbba3003f0f7f1ae81e97af31f/coverage-7.14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c050bbc7bba94c77e4ed7438f4fda1babe98ab145691d80aa6f60df934a1468b", size = 220883, upload-time = "2026-06-20T14:47:51.036Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f8/0e/d8f00efd3df0d63e6843ebcbade9e4119d60f5376753c9705d84b014c775/coverage-7.14.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a7af571767a2ee342a171c16fc1b1a07a0bf511606d381703fb7cf397fe49d46", size = 252395, upload-time = "2026-06-20T14:47:52.627Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1c/1c/ab9510dfe1a16a35a10f90efad0d9a9cf61b9876973752968f2ba882f73f/coverage-7.14.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8b4910cce599cd2438f8da65f5ef199a70a1cdb6ab314926df78271ca5954240", size = 255131, upload-time = "2026-06-20T14:47:54.235Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ba/dd/70171e9371003b33dc6b20f527ac216ff91bbe5c1088e754eb8950d79193/coverage-7.14.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c33e9e4878972f430b0cc06de3bf2a28d054a9efb4f8426d27de0d9cb81396ff", size = 256246, upload-time = "2026-06-20T14:47:55.61Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0f/80/a68b1dd81d5c011e17fd6ab0d707d33297df1d0c618114b9b750a2219c80/coverage-7.14.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7967ea55c6dea6becba4d5870e2fa0aa4915a8be7ebff1bb79e6207aa75ce8d", size = 258504, upload-time = "2026-06-20T14:47:56.979Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8e/7b/40baaa946189f5317cd77d484e39b9b0727d02ebada0a12162374f2faee2/coverage-7.14.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1322f237c2979b84096f4239c17828ff17fea6b3bbe96c44381c5f587c44c26", size = 252808, upload-time = "2026-06-20T14:47:58.418Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/05/b19517b09c43d1e8591de6c13178b0c03166c31e1adbebda378e64c66b9a/coverage-7.14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:77849525340c99f516d793dddbcee16b18d50af892ac43c8de1a6f343d41e3b5", size = 254166, upload-time = "2026-06-20T14:48:00.004Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ae/f5/6e65da5957e041d2094a9b97736628dd80160f1cc007a50790bbb2668c1a/coverage-7.14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef11695493ec3f06f7b2678ca274bcabb4ca04057317df268ddbfd8b05f661a8", size = 252310, upload-time = "2026-06-20T14:48:01.458Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2d/de/01b5274f0db63175b04d9354eff68d2d268b8b57a1b2db7d3dcb1f2c9dbb/coverage-7.14.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8134f0e0723e080d1c27bbe8fc149f0162e429fa1852482150015d0fce83eaf1", size = 256379, upload-time = "2026-06-20T14:48:02.981Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/71/d6/9a2ffbca41e2f8f86f61e8b78b86afa433ec8cdeac4908ace93a28fe3ff0/coverage-7.14.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:914eead2b843fc357f733b3fe39cc94f1b53d466e8cfe03080b1ed9d24ccfc73", size = 251880, upload-time = "2026-06-20T14:48:04.463Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/ff/20bd54a43c88c08f474e6cb355a97e024e38412873ef0a581629abe1e26f/coverage-7.14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4b2d5e847fb7958583b74910cc19e5ec4ece514487385677b26433b2546116e", size = 253753, upload-time = "2026-06-20T14:48:05.99Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/35/2a/2b3482c30d8344f301d8df6ff232a321f2ab87d5ac97ba21891a68638131/coverage-7.14.2-cp312-cp312-win32.whl", hash = "sha256:e753db9e40dda7302e0ac3e1e6e1325fb7f7b4694f87a7314ab15dd5d57911a7", size = 222584, upload-time = "2026-06-20T14:48:07.361Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f6/5e/83934ffff147edd313fe925db426e8f7ccad9e4663262eb5c4db4e345658/coverage-7.14.2-cp312-cp312-win_amd64.whl", hash = "sha256:d32e5ca5f16dafb269ee50b60d32b00c704b3f6f78e238105f1d94a3a5f24bf5", size = 223118, upload-time = "2026-06-20T14:48:08.837Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bb/ee/616b4f38a34f076f3045d3eedfa764d34d82e6a6cc6b300acb0f1ff22a98/coverage-7.14.2-cp312-cp312-win_arm64.whl", hash = "sha256:dc366f158e2fb2add9d4e57338ca48f12611024278688ee657eb0b853fcb5de5", size = 222504, upload-time = "2026-06-20T14:48:10.436Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6d/09/b5b334c27960e7aac0003b96491bada7838dc641099fa64a1a598abf33cd/coverage-7.14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e5f077641a6713ce9d38df9e85d4fb9e008677fc0775cbaeb32ddfc3b319d4ca", size = 220552, upload-time = "2026-06-20T14:48:11.847Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/79/20/879a000c319b4df7b50e4d688c0f7c0f6b5ac9d7b18848cbc00eabf26efe/coverage-7.14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0907f39b49ae818fe8af50aaa0f19afbc8ca164aea0865181ca7af17a3ac690b", size = 220919, upload-time = "2026-06-20T14:48:13.397Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f6/b7/326dded4371bab60f42215797944a356e4d81a3cee106121c7f7dd531604/coverage-7.14.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734d47669118d75c28981e562d4530ceb77342d31ffef6def5edd5ad4f05d7b", size = 251917, upload-time = "2026-06-20T14:48:14.931Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/eb/14/b3232ba218a0d1a70883d2675f18ff465de9e8e5e3346e81dc2b079838bd/coverage-7.14.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d9a1b5813d00ea6151f6ccf64d1fa16892771dfdda12ba87162d15ec4ea3e1e", size = 254515, upload-time = "2026-06-20T14:48:16.545Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b7/7a/d77bcbee1cad71b42776574114b462225cc9125b4982f43da1b66adc850f/coverage-7.14.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f0a80f4c8ac3f774210b1cc1bc0e31e75502f2818dda9a144ff90e702c4d91d", size = 255749, upload-time = "2026-06-20T14:48:18.214Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/86/86/97377937b29e9e44a1529bb20cb74dbcf80ed9006d87d7e742ff69e44b67/coverage-7.14.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e66f3f22d6c1515ce70f2e7c3e9c6f3ff0ff33480125c9f9c53e8f6508e30f", size = 257882, upload-time = "2026-06-20T14:48:19.7Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/a4/0fc8fe68bc505450bb068a2823ac7797bd8495240ccb8b4a5a1da1ee7e62/coverage-7.14.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6a2c37c3114f87ca7f10113756026eecb49656514debad600dcbec21f355ccea", size = 252144, upload-time = "2026-06-20T14:48:21.176Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8d/4a/450094ddc41ab0d2eb4a0457b3856400ea3329568d1303696e85de099ae6/coverage-7.14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b16a7959d04b1497281c062c180413565c3f3469211d78799ad5b9a75f67796", size = 253882, upload-time = "2026-06-20T14:48:22.701Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/28/2f6ae6d98265d9aa6bac311c4a93403675905b03aca95dc4373080279d75/coverage-7.14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6466c6999545cf00c4c142dfcbbf2db396dc735f005dcf8f91d57e351a79472b", size = 251846, upload-time = "2026-06-20T14:48:24.295Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/6e/707281468400794d52874e8fb5e38ff7578a0ff32ed49fe4fe85f192d0fc/coverage-7.14.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c60915ebb8f562317ba5ff6b8c32e25c0882289b201a9f2fb2987f91efd95d8", size = 256002, upload-time = "2026-06-20T14:48:26.015Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/83/5e963120de4011257a950ce4cfb7fc833ddf3fee19db495268d3dec28154/coverage-7.14.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:33b830850488acbcd358c78a4fecfafe7031667b4da8ddff5546295dc962cdeb", size = 251665, upload-time = "2026-06-20T14:48:27.654Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e9/78/66b482cd525083bcc0bc894c16db79dabac37490065b53b07d6e8ab77202/coverage-7.14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d0f845539230b8269aec902bc978b0cc403f52f002d18a04492efc943404d0bc", size = 253435, upload-time = "2026-06-20T14:48:29.354Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e6/61/0663fb8cb530c8b11819b920109694eee95a3b22960a9495be0200f657f1/coverage-7.14.2-cp313-cp313-win32.whl", hash = "sha256:a8ac51a2e441e9119b9395f4d893fbc4934c64c8ba58be9b9eaa85591249e548", size = 222591, upload-time = "2026-06-20T14:48:31.142Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a6/47/1536d2b009c2848c3682500f497053f4645e70911afe02f594000997831a/coverage-7.14.2-cp313-cp313-win_amd64.whl", hash = "sha256:039b264cdb31c44b48f9821e2afbf8f37df49e0fb837e24a942918b36c567e31", size = 223134, upload-time = "2026-06-20T14:48:32.696Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/28/9a/33ba4f335dd60bb34350318283d784f46018070e67b7d4df7c910ec9d9a0/coverage-7.14.2-cp313-cp313-win_arm64.whl", hash = "sha256:7f2ef591e381cc36b8e53334e1b842c760c520c8a52d01e8626209400e93fe6a", size = 222529, upload-time = "2026-06-20T14:48:34.237Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fc/bc/120390669817ede714ab141ae0a2a73240fd7354aac992c41dc0bd19570f/coverage-7.14.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7a0d1f026b72d627fa5c8a57cbc86ad209b64aa2a65833c83b290ace5cbee126", size = 220593, upload-time = "2026-06-20T14:48:35.755Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4f/a3/7f1cfacd76af91e585f7ad689d7168002b444ed2a8ce59f2daaff10089b5/coverage-7.14.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4d2b86f81c1c9310a7e774e3cc9e927a3d0bf583ecbfa01498dd626930025428", size = 220925, upload-time = "2026-06-20T14:48:37.35Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/10/6514b2525bb672eb8b43703e46d061d694111db21efe7609db722df2233f/coverage-7.14.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d76bdc1f9396ae70a55d050cf9743d88141c62ce0a22a3f627fab1d11c2f8bc6", size = 251974, upload-time = "2026-06-20T14:48:39.109Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/23/b4/4533091541c6620ecd68115bbfa1c61265b775618adef3a5fd137f4582e9/coverage-7.14.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cda36d8e7bfd63b3e44e75163265429caa5d935b672b00f71bccc8c010518c64", size = 254479, upload-time = "2026-06-20T14:48:40.871Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/06/af/e251a143d5d106385dbca696c553afab6b69f7f6bc376a34e089cc0b8b32/coverage-7.14.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0904f3b79d7b845bef0715afe1900da634d12b97f05b9479cb472880ca07cb9c", size = 255824, upload-time = "2026-06-20T14:48:42.608Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9c/53/9e5876e60efbaa79d743d1948a5015ddc05b808db1cd62228acf83e87d43/coverage-7.14.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6795ca4198d6cb7fc2c6163214f6555a6bc5f0ae1e268e76139dec4b37c4499", size = 258139, upload-time = "2026-06-20T14:48:44.263Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/85/5a/d35a4f431fb594e46b81cad4a13b470b017e918f347c1c0b260f7494fa1e/coverage-7.14.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c41e9b60fc0fa57f5d73306417d2f9d668202cca6944f9435878c55a5e7ae213", size = 252002, upload-time = "2026-06-20T14:48:45.961Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/e2/f5b304c8139c606c4f1b230d3a257d0c88edfbbdf06c58364f07625dc45c/coverage-7.14.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419d2aadd5746efc2e9df0f33c05570d8192e6f6a6098ab05acce586f44ce8a5", size = 253832, upload-time = "2026-06-20T14:48:47.582Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/86/bc/bbbd283daa6be4f68aad4ad4066fd39ae98e4174db8c03ab26c5803d6234/coverage-7.14.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c5d273c5f1411c0d26c4f066c398d4a434b1f97bb5fa409189bedce86d4add4", size = 251799, upload-time = "2026-06-20T14:48:49.42Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/69/8d/0745fceb89c9e5f7dd8ed243d97dc8561b7a95545741e2409d2b34654824/coverage-7.14.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5fe465bc691264adce601527a972990c1174075d86bcbe9968fd20c95e0b1948", size = 256075, upload-time = "2026-06-20T14:48:51.065Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a2/a0/441d9a5255cf021ab41ee00c014a4607d1c72d5e5bef0a4fdaa5be86a907/coverage-7.14.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6fbb61617af1c56f95d53170ae9fa6c9aef6de1abd02fcc50064bfc672efb18d", size = 251612, upload-time = "2026-06-20T14:48:52.653Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/50/37/3d19c5e32d4a529c068eb296abfa3e455bd2c0f9311ecf26280f408ff8e0/coverage-7.14.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e1eff22b831dfd5694989cc1f0789980f18391f614ac67c851af9a8e6d25e9ba", size = 253270, upload-time = "2026-06-20T14:48:54.3Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3d/b0/54dd13937297518da6d092cc2c39d9340ec2194bdfa92e0a64694d643e23/coverage-7.14.2-cp314-cp314-win32.whl", hash = "sha256:58e91be0a233adef698d3e6be54f10401bb91fd7854c0d4c4d50e0d3711e72f1", size = 222796, upload-time = "2026-06-20T14:48:56.084Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/51/45/7a10e0909919686e335fdd95869cfb222d55243ebff27dc5cf59ca259a1f/coverage-7.14.2-cp314-cp314-win_amd64.whl", hash = "sha256:d8429bf97906bfe6c61f9dbfb3342e0d88120da61939da8bd04f830cc3eab3b8", size = 223285, upload-time = "2026-06-20T14:48:57.729Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2e/03/9cb197eb4b3d1a2eccb2537c226a93c80522c5b8afc5dd93e1993d7bb021/coverage-7.14.2-cp314-cp314-win_arm64.whl", hash = "sha256:13609d9d77249447aa73357b14831b0f3b95f275026c9ff20dd105f981f53a0c", size = 222712, upload-time = "2026-06-20T14:48:59.413Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d6/3c/e59f498511080d20bf866b0af9eeab820feb91547dae2084cb9bb7fb0e58/coverage-7.14.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9818486c2bac88ae931df7e04905ee29bef49fd218c00f5f02bed4855254a101", size = 221325, upload-time = "2026-06-20T14:49:01.447Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d3/37/8d7955f7e701e69198bd0a0132ea76518c078a635b930a4924e2ccfa70f0/coverage-7.14.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:58055adffabfa243516a197aa9f85f0dd56d905b0fba1a10193269759c29ccb0", size = 221594, upload-time = "2026-06-20T14:49:03.13Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/34/7a/6738e1e1533ce8ec4e2e472696eefdd4723864d7efaa140e433053bf576a/coverage-7.14.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:535747dbc200349d7fb434cffcb28e770f0290f69b225f56dc3803aa7210cdea", size = 262957, upload-time = "2026-06-20T14:49:04.829Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/35/c4/d1be863cd39e0955904315fece67c5c23e046563f5eea0ceac16c547a759/coverage-7.14.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:420c66e35d85c0ca5dc6a38147d83ef239762542900e5921ebbdb89333c540ea", size = 265081, upload-time = "2026-06-20T14:49:07.018Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/7f/412df3c3c251284a11834287fd6f7e3bb98c528c53e030589e9344a3ef80/coverage-7.14.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2cf17b33773be446a588551ea6a746b2d70dd0bc90dc31f1dd7648975a63c6b", size = 267500, upload-time = "2026-06-20T14:49:08.709Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/54/68/7d0764e83459455384d5c04179ce2d2a837bef01b9ba463079c6e8b31361/coverage-7.14.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:adb4a5fef041f7179bb264203add873c147d169cf2f8d0adae89ff2e51271bac", size = 268619, upload-time = "2026-06-20T14:49:10.405Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/14/68/1292164ac70cbcc86ac3982da31a6fbb42bb4bcebf6e5cf73c99cfcfd50d/coverage-7.14.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c012ec357dec9408a83dad5541172a63c5cfa1421709f2e5811480d31ae1b28", size = 262066, upload-time = "2026-06-20T14:49:12.257Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/44/fd6fdf3f63b6e00a1a9230022d072ded5189576001685706aa6524187c65/coverage-7.14.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dacd0ecd08fda3cb2f85b60cabea7da326dcb2fc15fbb23a88830a80144cc9f2", size = 264953, upload-time = "2026-06-20T14:49:14.13Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/39/29/e803fea3da89eaeb5b6b41b3ccd039fe9f3300a900e3803baac1a998529f/coverage-7.14.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f27e980f2feba5dfe7a32b22b125470de69c0bd113c75e16165de909a777f512", size = 262555, upload-time = "2026-06-20T14:49:15.803Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/32/3c/b360e48ac68e3236c04cb83658382e7f5be7efbbec2e1faae3dcca432783/coverage-7.14.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:105c00efb65c863630b2b63cbf7b8267e4da2d44b62284efbb19a03b04c337d4", size = 266289, upload-time = "2026-06-20T14:49:17.962Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/59/12/1ed6d9274d599c586e2d1aa9818765dcdae6bb52aa88afa2fcd868398191/coverage-7.14.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:571173fa04c8e8d6235ab32ae67fecca97777e2e1b4a1a30f3022c34e397c1c1", size = 261402, upload-time = "2026-06-20T14:49:19.708Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/44/17/eb6cf12a4538cda937aefbeabb15377a8a30b377b484e63d31c9da790966/coverage-7.14.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e532f34d42d1a421fa00ed6b7735d14ac2e340256c1bad26a5e1dc1252b0bed7", size = 263715, upload-time = "2026-06-20T14:49:21.427Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8a/ca/4bafdb9d372ab05d6ed3a63e7f00d3195d169d0afea00f617c026e386c19/coverage-7.14.2-cp314-cp314t-win32.whl", hash = "sha256:243971550fb46c3039257f75e65610002d84304c505f609bbd9779e20a653a0a", size = 223103, upload-time = "2026-06-20T14:49:23.24Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/35/cb/0765dbd9011d2e47315f1da31e62c5fe231f04a6ec8da213e64c4505896d/coverage-7.14.2-cp314-cp314t-win_amd64.whl", hash = "sha256:60fb0ca084a92da96474b8b405a7ea76dfecac3c68db54383e7934b6f3871169", size = 223934, upload-time = "2026-06-20T14:49:25.347Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4e/ce/373dde027ecd0ae54511430fe7569f838d3a0376b70333ba9fd20c76b836/coverage-7.14.2-cp314-cp314t-win_arm64.whl", hash = "sha256:36a0a3f42ed7dfdbca2a69a541519ffd5064a5692152fc0018109e74370d7345", size = 223249, upload-time = "2026-06-20T14:49:27.241Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a3/5e/a8ba14ceb014f39bd5e3f7077150718c7de61c01ce326bfe7e8eae9b19b2/coverage-7.14.2-py3-none-any.whl", hash = "sha256:04d92589e481a8b68a005a5a1e0646a91c76f322c397c4635298c57cf63699b5", size = 212325, upload-time = "2026-06-20T14:49:28.991Z" }, ] [package.optional-dependencies] @@ -704,64 +704,64 @@ toml = [ [[package]] name = "cryptography" version = "49.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, - { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, - { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, - { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, - { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, - { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, - { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, - { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, - { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, - { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, - { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, - { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, - { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, - { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, - { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, - { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, - { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, - { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, - { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, - { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, - { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, - { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, - { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, - { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, ] [[package]] name = "cyclonedx-bom" version = "7.3.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "chardet" }, { name = "cyclonedx-python-lib", extra = ["validation"] }, @@ -770,159 +770,161 @@ dependencies = [ { name = "pip-requirements-parser" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/e1/700aea05811f8988a60636e045914ffb155ce5318879f14b5c9016a12da5/cyclonedx_bom-7.3.0.tar.gz", hash = "sha256:d54080d65731980945de38e52009a5e5711f4a3c31377a3d13af96eaca5fcf3d", size = 4420319, upload-time = "2026-03-30T12:30:08.438Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6c/e1/700aea05811f8988a60636e045914ffb155ce5318879f14b5c9016a12da5/cyclonedx_bom-7.3.0.tar.gz", hash = "sha256:d54080d65731980945de38e52009a5e5711f4a3c31377a3d13af96eaca5fcf3d", size = 4420319, upload-time = "2026-03-30T12:30:08.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/f8/285a990b17b44dac4899b14fbeab81a28f3bb828621d4a6c449631249136/cyclonedx_bom-7.3.0-py3-none-any.whl", hash = "sha256:73d7d76b3a28ebadc50eabf424cbcf128785a74b8a59ce7893da2e29cfaab585", size = 60924, upload-time = "2026-03-30T12:30:06.943Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c6/f8/285a990b17b44dac4899b14fbeab81a28f3bb828621d4a6c449631249136/cyclonedx_bom-7.3.0-py3-none-any.whl", hash = "sha256:73d7d76b3a28ebadc50eabf424cbcf128785a74b8a59ce7893da2e29cfaab585", size = 60924, upload-time = "2026-03-30T12:30:06.943Z" }, ] [[package]] name = "cyclonedx-python-lib" -version = "9.1.0" -source = { registry = "https://pypi.org/simple" } +version = "11.11.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "license-expression" }, { name = "packageurl-python" }, { name = "py-serializable" }, { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/fc/abaad5482f7b59c9a0a9d8f354ce4ce23346d582a0d85730b559562bbeb4/cyclonedx_python_lib-9.1.0.tar.gz", hash = "sha256:86935f2c88a7b47a529b93c724dbd3e903bc573f6f8bd977628a7ca1b5dadea1", size = 1048735, upload-time = "2025-02-27T17:23:40.367Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/75/c9/5d0ccdd19bc7d8ab803b90695c1706aa2ea8529685d18e682dc2524d2630/cyclonedx_python_lib-11.11.0.tar.gz", hash = "sha256:4b3194db72b613717f2912447e67ab618c75ff7dcac6c4af3c0e9e1ac617c102", size = 1442983, upload-time = "2026-06-17T11:57:49.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/f1/f3be2e9820a2c26fa77622223e91f9c504e1581830930d477e06146073f4/cyclonedx_python_lib-9.1.0-py3-none-any.whl", hash = "sha256:55693fca8edaecc3363b24af14e82cc6e659eb1e8353e58b587c42652ce0fb52", size = 374968, upload-time = "2025-02-27T17:23:37.766Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/f3/56ccb2884aaa3db5622368e5191a3384b15f35392aa93df8b2f508c660d2/cyclonedx_python_lib-11.11.0-py3-none-any.whl", hash = "sha256:3049fc83e06a059b5c5907a527625a8ed5073caab10607ed4c9e5503b590fd44", size = 528689, upload-time = "2026-06-17T11:57:47.358Z" }, ] [package.optional-dependencies] validation = [ - { name = "jsonschema", extra = ["format"] }, + { name = "jsonschema", extra = ["format-nongpl"] }, { name = "lxml" }, + { name = "referencing" }, ] [[package]] name = "decorator" version = "5.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] [[package]] name = "defusedxml" version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] [[package]] name = "deprecated" version = "1.3.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] [[package]] name = "distlib" version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, ] [[package]] name = "docker" version = "7.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "requests" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, ] [[package]] name = "docutils" version = "0.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e", size = 2303823, upload-time = "2026-05-27T17:41:06.934Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e", size = 2303823, upload-time = "2026-05-27T17:41:06.934Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", size = 634701, upload-time = "2026-05-27T17:40:58.442Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", size = 634701, upload-time = "2026-05-27T17:40:58.442Z" }, ] [[package]] name = "dotty-dict" version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/ab/88d67f02024700b48cd8232579ad1316aa9df2272c63049c27cc094229d6/dotty_dict-1.3.1.tar.gz", hash = "sha256:4b016e03b8ae265539757a53eba24b9bfda506fb94fbce0bee843c6f05541a15", size = 7699, upload-time = "2022-07-09T18:50:57.727Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/ab/88d67f02024700b48cd8232579ad1316aa9df2272c63049c27cc094229d6/dotty_dict-1.3.1.tar.gz", hash = "sha256:4b016e03b8ae265539757a53eba24b9bfda506fb94fbce0bee843c6f05541a15", size = 7699, upload-time = "2022-07-09T18:50:57.727Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" }, ] [[package]] name = "dparse" version = "0.6.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/ee/96c65e17222b973f0d3d0aa9bad6a59104ca1b0eb5b659c25c2900fccd85/dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a", size = 27912, upload-time = "2024-11-08T16:52:06.444Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/29/ee/96c65e17222b973f0d3d0aa9bad6a59104ca1b0eb5b659c25c2900fccd85/dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a", size = 27912, upload-time = "2024-11-08T16:52:06.444Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/26/035d1c308882514a1e6ddca27f9d3e570d67a0e293e7b4d910a70c8fe32b/dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57", size = 11925, upload-time = "2024-11-08T16:52:03.844Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/56/26/035d1c308882514a1e6ddca27f9d3e570d67a0e293e7b4d910a70c8fe32b/dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57", size = 11925, upload-time = "2024-11-08T16:52:03.844Z" }, ] [[package]] name = "exceptiongroup" version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, ] [[package]] name = "execnet" version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] name = "executing" version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[package]] name = "face" version = "26.0.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "boltons" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/fd/f84f0600bd72953d5a322f0dedbd4f900e2cedab718e6b6a093ae2d16aae/face-26.0.1.tar.gz", hash = "sha256:8183d94bc248baaea855a9f8445f97a22a9988908e60abddccc6e251da77c4c6", size = 51754, upload-time = "2026-06-17T23:14:39.938Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/53/fd/f84f0600bd72953d5a322f0dedbd4f900e2cedab718e6b6a093ae2d16aae/face-26.0.1.tar.gz", hash = "sha256:8183d94bc248baaea855a9f8445f97a22a9988908e60abddccc6e251da77c4c6", size = 51754, upload-time = "2026-06-17T23:14:39.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/24/0159c48d19c8b05e6969ee809bbbecc5a5c863f7e38c9327e2c63cb06f0f/face-26.0.1-py3-none-any.whl", hash = "sha256:ab0a83c37c9789dce658a67a9a80eafaa113c9ec37c5a9d950ff5480542a062d", size = 57572, upload-time = "2026-06-17T23:14:38.711Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0a/24/0159c48d19c8b05e6969ee809bbbecc5a5c863f7e38c9327e2c63cb06f0f/face-26.0.1-py3-none-any.whl", hash = "sha256:ab0a83c37c9789dce658a67a9a80eafaa113c9ec37c5a9d950ff5480542a062d", size = 57572, upload-time = "2026-06-17T23:14:38.711Z" }, ] [[package]] name = "fastapi" version = "0.138.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, @@ -930,45 +932,45 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/58/ff455d9fe47c60abadb34b9e05a304b1f05f5ab8000ac01565156b6f5e43/fastapi-0.138.0.tar.gz", hash = "sha256:d445a4877636ad191e7053e08c9bf98cb921a6756776848400bb773d1740c061", size = 419240, upload-time = "2026-06-20T01:18:05.259Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5b/58/ff455d9fe47c60abadb34b9e05a304b1f05f5ab8000ac01565156b6f5e43/fastapi-0.138.0.tar.gz", hash = "sha256:d445a4877636ad191e7053e08c9bf98cb921a6756776848400bb773d1740c061", size = 419240, upload-time = "2026-06-20T01:18:05.259Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/ff/8496d9847a5fedae775eb49460722d3efaa80487854273e9647ae876218c/fastapi-0.138.0-py3-none-any.whl", hash = "sha256:b6f54fd1bd72c80b0f899f172c61a600f6f7af9b43d4d772a018f35624048cb0", size = 126779, upload-time = "2026-06-20T01:18:03.483Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6c/ff/8496d9847a5fedae775eb49460722d3efaa80487854273e9647ae876218c/fastapi-0.138.0-py3-none-any.whl", hash = "sha256:b6f54fd1bd72c80b0f899f172c61a600f6f7af9b43d4d772a018f35624048cb0", size = 126779, upload-time = "2026-06-20T01:18:03.483Z" }, ] [[package]] name = "filelock" version = "3.29.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, ] [[package]] name = "fqdn" version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, ] [[package]] name = "ghp-import" version = "2.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] [[package]] name = "git-changelog" version = "2.9.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "jinja2" }, { name = "packaging" }, @@ -977,252 +979,273 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/7f/17e58ec656fd79d4a3b02517923e24a93b28fe2e4518568aedb4ce038fb9/git_changelog-2.9.4.tar.gz", hash = "sha256:06a38a0d174a069b760430ec41493bf53307be13f16b0e48ce379be1646ea9a0", size = 92601, upload-time = "2026-04-29T14:01:39.575Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/01/7f/17e58ec656fd79d4a3b02517923e24a93b28fe2e4518568aedb4ce038fb9/git_changelog-2.9.4.tar.gz", hash = "sha256:06a38a0d174a069b760430ec41493bf53307be13f16b0e48ce379be1646ea9a0", size = 92601, upload-time = "2026-04-29T14:01:39.575Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/a6/fb6713ce5c128078492b5ce691bd27b8a7c6f2ad84aca99d3b847ab1721d/git_changelog-2.9.4-py3-none-any.whl", hash = "sha256:c19a099640518fbbc545cb240250cf8129e55e668d8374ba8061ea5456da204b", size = 40604, upload-time = "2026-04-29T14:01:38.195Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ec/a6/fb6713ce5c128078492b5ce691bd27b8a7c6f2ad84aca99d3b847ab1721d/git_changelog-2.9.4-py3-none-any.whl", hash = "sha256:c19a099640518fbbc545cb240250cf8129e55e668d8374ba8061ea5456da204b", size = 40604, upload-time = "2026-04-29T14:01:38.195Z" }, ] [[package]] name = "gitdb" version = "4.0.12" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "smmap" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, ] [[package]] name = "gitpython" version = "3.1.50" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] [[package]] name = "glom" -version = "22.1.0" -source = { registry = "https://pypi.org/simple" } +version = "25.12.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "attrs" }, { name = "boltons" }, { name = "face" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/d1/69432deefa6f5283ec75b246d0540097ae26f618b915519ee3824c4c5dd6/glom-22.1.0.tar.gz", hash = "sha256:1510c6587a8f9c64a246641b70033cbc5ebde99f02ad245693678038e821aeb5", size = 189738, upload-time = "2022-01-24T09:34:04.874Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/74/8387f95565ba7c30cd152a585b275ebb9a834d1d32782425c5d2fe0a102c/glom-25.12.0.tar.gz", hash = "sha256:1ae7da88be3693df40ad27bdf57a765a55c075c86c971bcddd67927403eb0069", size = 196128, upload-time = "2025-12-29T06:29:07.274Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a7/e6/4129d9a3baa72d747533bb33376543ccadd9a7f9944e5a6e3ae2e245f5d6/glom-25.12.0-py3-none-any.whl", hash = "sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1", size = 103295, upload-time = "2025-12-29T06:29:06.074Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/e8/68e274b2a30e1fdfd25bdc27194382be3f233929c8f727c0440d58ac074f/glom-22.1.0-py2.py3-none-any.whl", hash = "sha256:5339da206bf3532e01a83a35aca202960ea885156986d190574b779598e9e772", size = 100687, upload-time = "2022-01-24T09:34:02.391Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [[package]] name = "graphql-core" version = "3.2.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/90/f2aff026ab4aebd80eb71905106a0885f4cfde85dcf965543f45bed0d9ee/graphql_core-3.2.11.tar.gz", hash = "sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802", size = 528407, upload-time = "2026-06-05T13:45:22.915Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4d/90/f2aff026ab4aebd80eb71905106a0885f4cfde85dcf965543f45bed0d9ee/graphql_core-3.2.11.tar.gz", hash = "sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802", size = 528407, upload-time = "2026-06-05T13:45:22.915Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl", hash = "sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0", size = 214879, upload-time = "2026-06-05T13:45:21.245Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl", hash = "sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0", size = 214879, upload-time = "2026-06-05T13:45:21.245Z" }, ] [[package]] name = "greenlet" version = "3.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/8b/befc3cb36965f397d87e86fb3b00e3ec0dc67c1ecb0986d7f54ee528f018/greenlet-3.5.2.tar.gz", hash = "sha256:c1b906220d83c140361cdd12eef970fb5881a168b98ee58a43786426173da14c", size = 199243, upload-time = "2026-06-17T20:19:01.317Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/3a/cd99db55dc908568f6b91845747b98b3b17a06052fa1803d091dc91da27d/greenlet-3.5.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9df9daae96848508450011d0d86ed7c95f8829a354ce438284a77b24896fd1f8", size = 285626, upload-time = "2026-06-17T17:33:33.231Z" }, - { url = "https://files.pythonhosted.org/packages/ce/09/fd997a19cbb97641233c7d5f8fc89314c132be2c8867c4f14beff979996f/greenlet-3.5.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01e32e9d2b1714a2b06184cb3071ff2a2fd9bc7d065e39198ab21f7253dad421", size = 601821, upload-time = "2026-06-17T18:07:16.756Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b0/62abd204addd913ad9856e091f5d8baaedc7c85df151f22f093b8a207c20/greenlet-3.5.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0488ca77c94da5e09d1d9958f98b58cebba1b8fd9664c24898499133de927574", size = 615044, upload-time = "2026-06-17T18:29:39.344Z" }, - { url = "https://files.pythonhosted.org/packages/34/67/ceaab731b51611a8238b0af2d4abb4fd727ec09b16cd499fca5295603f46/greenlet-3.5.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d9e19257794e28821c9ebd5e23f86d7c267cd9d390089374f068d2049f949e3", size = 615176, upload-time = "2026-06-17T17:39:25.134Z" }, - { url = "https://files.pythonhosted.org/packages/1c/40/51a0ee73b72a7e4a65b54433316bbd7b3b7902a585310cd4e3051d411ee3/greenlet-3.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bf493b3c1c0a2324c49b0472e2280ba4665f3510d8115f6f807759a6163b15f7", size = 1574580, upload-time = "2026-06-17T18:22:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/41/d3/a3a2163b1fe73042d3e72cfcb9920f2481d5188a1df2645587a9b83a903f/greenlet-3.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:561dd919c02236a613fbf226791cbd77ee5002cbd5cb7e838869aa3ac7a71e16", size = 1641192, upload-time = "2026-06-17T17:40:04.234Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/b4d83fb451e2f7266cb45ccef23857f8a800e0a5d9a73263fafdf7ba7904/greenlet-3.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:049827baab63dda8ab8ec5a6d07fc6eb0f418319cfc757fc8737a605e99ca1ad", size = 238247, upload-time = "2026-06-17T17:34:54.794Z" }, - { url = "https://files.pythonhosted.org/packages/21/68/371ee6dad168be3386c46030bedaa8e3e7e3cf3d203621d4529e78ff36ef/greenlet-3.5.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d7792398872f89466c6671d5d193537eff163ecf7fac78d82e6ddc25017fb4f5", size = 286925, upload-time = "2026-06-17T17:33:17.928Z" }, - { url = "https://files.pythonhosted.org/packages/26/16/ed5706c26b4d26f3fabceb79abca992654eac8b0fa435def2ac6dbd92122/greenlet-3.5.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:711028c953cd6ce5dc01bbb5a1747e3ad6bd8b2f7ded73778bb936e8dab9e3b6", size = 606036, upload-time = "2026-06-17T18:07:18.538Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/f9c77093af9f5f96615922b7e3fe3690a9faff02adb89f1d74e21578b147/greenlet-3.5.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5eba55076d79e8a5176e6925295cfb901ebc95dae493342ede22230f75d8bee2", size = 617821, upload-time = "2026-06-17T18:29:41.317Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d4/642833e778c17d32b5cabb793e14ce7364c55952462fc506fecdee55d485/greenlet-3.5.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1c1e5ad80f1f38ea479b83b39dccb20874cfe9ad5e52f87225fa294ba4d39a1", size = 616877, upload-time = "2026-06-17T17:39:26.564Z" }, - { url = "https://files.pythonhosted.org/packages/d3/cc/7120f83e78b8be3cf7acbe2306b3b7bd2cbf99f5ad12e85e2f05d7b31961/greenlet-3.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e194b996aa1b89d933cfe136e5eb39b22a8b72ba59d376ef39a55bca4dbf47f", size = 1577274, upload-time = "2026-06-17T18:22:10.692Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d8/05a0074ee485dd51c320fd706fd7ed48006b9cad3443092d7df1a655f0d2/greenlet-3.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e554809538bd4867f24421b43abde170f9c9b8192149b30df5e164bcac6124f", size = 1643566, upload-time = "2026-06-17T17:40:05.452Z" }, - { url = "https://files.pythonhosted.org/packages/35/fe/9fe2060bdeece682e38d381184ae66045b48ed183c107ab3f88b9886a630/greenlet-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:e063263ce9047878480d7e536012fc8b7c8e1922989eb5f03b9ab998a2ee7b7e", size = 238643, upload-time = "2026-06-17T17:37:03.039Z" }, - { url = "https://files.pythonhosted.org/packages/41/13/a9db72f5b6b700977ebd371d6a1f2984a08838357de924fcd5571607b1bf/greenlet-3.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:a3f76a94e2d6e1fee8f302265679d8cc47d71a203936dd03c6e2ace0f9cfd46d", size = 237135, upload-time = "2026-06-17T17:34:34.14Z" }, - { url = "https://files.pythonhosted.org/packages/3f/7a/6bc2a7835731387ed303b9390ce68a116ab053df05450a59181239200454/greenlet-3.5.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:76dae33e97b52743a19210931ee3e78a88fe1438bc2fc4ee5e7512d289bfad4f", size = 288351, upload-time = "2026-06-17T17:36:17.019Z" }, - { url = "https://files.pythonhosted.org/packages/57/1b/bd98062fcef6d0e9d0873ab6f2d029772e6ea342972ae43275bd6177900f/greenlet-3.5.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30252d191d6959df1d040b559a38fc017139606c5ecc2ad00416557c0355d742", size = 604273, upload-time = "2026-06-17T18:07:20.296Z" }, - { url = "https://files.pythonhosted.org/packages/25/e6/fe392c522bf45d976abe7db2793f6ef4e87b053ebb869deeaae46aeb54da/greenlet-3.5.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1adc23c50f22b0f5979521909a8360ab4a3d3bef8b641ce633a04cf1b1c967ea", size = 616536, upload-time = "2026-06-17T18:29:43.205Z" }, - { url = "https://files.pythonhosted.org/packages/68/4a/399ff81fa93a19d6a9df394cef0355f082dbc19ad41aba9593cd0ad444e2/greenlet-3.5.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f052fff492c52fdfa99bd3b3c1389a53de37dae76a0562741417f0d018f02b3", size = 613749, upload-time = "2026-06-17T17:39:28.148Z" }, - { url = "https://files.pythonhosted.org/packages/a5/75/f519593f12ad43d08e28c03a95cfe2eeae011707dbc9dab0c4a263ce90f9/greenlet-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:120b77c2a18ebf629c3a7886f68c6d01e065654844ad468f15bb93ace66f2094", size = 1573725, upload-time = "2026-06-17T18:22:12.023Z" }, - { url = "https://files.pythonhosted.org/packages/f1/bc/bc1ea4b0754c6c51bbf9d94677b0b1f7fbda8cbb404e44a896854fc0a940/greenlet-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a850f6224088ef7dcc70f1a545cb6b3d119c35d6dca63b925b9f35da0635cdad", size = 1638132, upload-time = "2026-06-17T17:40:06.971Z" }, - { url = "https://files.pythonhosted.org/packages/36/c0/f0f5a34247df60de285f75f22e57f14027f4b3c43820981854b5b643ca6d/greenlet-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:89da99ee8345b458ea2f16831dad31c88ddcdec454b48704d569a0b8fb28f146", size = 239393, upload-time = "2026-06-17T17:33:47.09Z" }, - { url = "https://files.pythonhosted.org/packages/09/17/a8544e165445f30aea67a8d9cf2786d2bb0eb1b0e0d224b4d9bd80e2d587/greenlet-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:ca92411942154023c65851e6077d8ca0d00f19de5fa80bb2c6f196ff6c920ba9", size = 237723, upload-time = "2026-06-17T17:36:47.776Z" }, - { url = "https://files.pythonhosted.org/packages/d0/3c/bb37b9d40d65b0741a8b040ca5c307034d0a9822994dff5f825c88dd7a6b/greenlet-3.5.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0629377725977252159de1ebd3c6e49c170a63856e585446797bb3d66d4d9c34", size = 287178, upload-time = "2026-06-17T17:35:25.132Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a6/0c5902393f492f8ceb19d0b5cf139284e3a11b333a049739643b1036b6f8/greenlet-3.5.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2ddf9eddc617681108dd071b3feabf3f4a4cd64846254aec4d4ceda098b639a", size = 606900, upload-time = "2026-06-17T18:07:21.692Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7c/42899c31d4b87148ae4e3f87f63e13398824be6241f4dde42ded95768a34/greenlet-3.5.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f41feb9f2b59e2e61ac9bea4e344ddd9396bf3cacb2583f73a3595ed7df6f8e7", size = 619265, upload-time = "2026-06-17T18:29:44.837Z" }, - { url = "https://files.pythonhosted.org/packages/d3/52/4ff8c98d3cfe62b4515f8584ae14510a58f35c549cc5292b78d9b7a40b70/greenlet-3.5.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09201fa698768db245920b00fdc86ee3e73540f01ca6db162be9632642e1a473", size = 616187, upload-time = "2026-06-17T17:39:29.473Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a6/269c8bf9aefc13361ce1088f0e392b154cb21005de7862e42b5d782b81fd/greenlet-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1759fa4f14c398508cf20dc8037de55cc23ae8bd14c185c2718257837195ca5", size = 1573778, upload-time = "2026-06-17T18:22:13.497Z" }, - { url = "https://files.pythonhosted.org/packages/1f/9b/391d015cbc6323e81b14c02cf825fdca7e0049c9bb489bf4ac72883118ba/greenlet-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9318cdeb9abdbfdd8bc8464ee4a06dffde2c7846e1def138365a6240ab2c9a5", size = 1638092, upload-time = "2026-06-17T17:40:08.163Z" }, - { url = "https://files.pythonhosted.org/packages/49/53/5b4df711f4356c62e85d9f819d87966d526d1cfb32bae49a8f7d6fc36ea4/greenlet-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:2c3b3311af72b3d3b03cc0f1ffd11f072e834be5d0444105cf715fc44434e39c", size = 239352, upload-time = "2026-06-17T17:38:51.593Z" }, - { url = "https://files.pythonhosted.org/packages/bb/b6/18efc3a329ec035c3f344b8f2b60356451950ddf9b7b64ff00023778a1dd/greenlet-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:f9bbd6216c45a563c2a61e478e038b439d9f248bde44f775ea37d339da643af4", size = 237635, upload-time = "2026-06-17T17:35:36.632Z" }, - { url = "https://files.pythonhosted.org/packages/c7/89/aaafc8e14de4ac882e02ccb963225329b0e8578aba4365e71eb678e45722/greenlet-3.5.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", size = 287676, upload-time = "2026-06-17T17:33:31.514Z" }, - { url = "https://files.pythonhosted.org/packages/b8/fc/2308249206c12ac70de7b9a00970f84f07d10b3cd60e05d2fbcaa84124e8/greenlet-3.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", size = 653552, upload-time = "2026-06-17T18:07:23.493Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/47730d1f8f1336b9b089237521ed7a26eee997065dcb4cab81cdca333abc/greenlet-3.5.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", size = 665756, upload-time = "2026-06-17T18:29:46.616Z" }, - { url = "https://files.pythonhosted.org/packages/99/69/d6c99db15dc0b5e892ac3cc7b942c8b21f4a9cc3bd9ea0bc3b0f339ffbd4/greenlet-3.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", size = 663228, upload-time = "2026-06-17T17:39:31.073Z" }, - { url = "https://files.pythonhosted.org/packages/4f/88/9e603f448e2bc107c883e95817b980fb9b45ba6aea0299b2e9978124bea2/greenlet-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", size = 1620723, upload-time = "2026-06-17T18:22:14.817Z" }, - { url = "https://files.pythonhosted.org/packages/11/91/26da17e3777858c16fdb8d020a4c68f3a03cb92f238de8f5351d5d5186e9/greenlet-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", size = 1684227, upload-time = "2026-06-17T17:40:09.536Z" }, - { url = "https://files.pythonhosted.org/packages/2d/44/b3a11f7aa34cb38f1b7f3df8bcd9fcd09bac9d342c2a2c9b8686c804bcd2/greenlet-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", size = 240257, upload-time = "2026-06-17T17:35:23.359Z" }, - { url = "https://files.pythonhosted.org/packages/de/e3/3b62145fe917311732041a258adb218248add00542e3131c48bd047fbed5/greenlet-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3c417cd6c593bbbef6f7aa31a79f37d3db7d18832fc56b694a2150130bde784e", size = 239038, upload-time = "2026-06-17T17:37:56.792Z" }, - { url = "https://files.pythonhosted.org/packages/47/ac/d3bad483e9f6cd1848604fdffa32cac25846dd6dfcec0e6f81c790185518/greenlet-3.5.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", size = 295668, upload-time = "2026-06-17T17:36:02.293Z" }, - { url = "https://files.pythonhosted.org/packages/00/e9/3a7e557b895fd0469b00cd0b2bd498ba950e8bfdf6d7adeecf2c5e4130a6/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", size = 652820, upload-time = "2026-06-17T18:07:24.95Z" }, - { url = "https://files.pythonhosted.org/packages/78/67/6225d5c5e4afc04be0fd161eec82e4b72017e8a100d222f25d7b42b0140d/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", size = 658697, upload-time = "2026-06-17T18:29:48.365Z" }, - { url = "https://files.pythonhosted.org/packages/fa/99/6324b8ef916dcaddccb340b304c992ca3f947614ce0f2685d438187300b8/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", size = 656436, upload-time = "2026-06-17T17:39:32.509Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ee/f5bf9daac27c5e1b011965f64b5630a32b415daf7381b312943629e12c2a/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", size = 1617193, upload-time = "2026-06-17T18:22:16.252Z" }, - { url = "https://files.pythonhosted.org/packages/8a/21/b05d5b12715bda92ce27c118d64971d21e9b8f3563ed959a7d271e2d4223/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", size = 1677512, upload-time = "2026-06-17T17:40:10.771Z" }, - { url = "https://files.pythonhosted.org/packages/b8/97/1b8f1314b868041b327dc1051603e8142b826480cb0ecb8a7b7632aee9c4/greenlet-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", size = 243145, upload-time = "2026-06-17T17:34:37.502Z" }, - { url = "https://files.pythonhosted.org/packages/36/07/1b5311775e04c718a118c504d7a3a312430e2a1bd1347226aff4774e4549/greenlet-3.5.2-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", size = 288315, upload-time = "2026-06-17T17:34:34.04Z" }, - { url = "https://files.pythonhosted.org/packages/ed/cc/6abcd2a486b58b9f77b7a93b690d59cb2c11a5906ed2ad4c63c7b9c1113d/greenlet-3.5.2-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", size = 659130, upload-time = "2026-06-17T18:07:26.354Z" }, - { url = "https://files.pythonhosted.org/packages/f2/12/f4aaad6d3d383233f700ab322568a4f29f2c701a4861d85f4811d99689b2/greenlet-3.5.2-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", size = 669724, upload-time = "2026-06-17T18:29:50.13Z" }, - { url = "https://files.pythonhosted.org/packages/91/2a/a089811fc31c6bf8742f40a4e73470d6d401cef18e4314eb20dc399b377c/greenlet-3.5.2-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", size = 668089, upload-time = "2026-06-17T17:39:33.808Z" }, - { url = "https://files.pythonhosted.org/packages/0f/1c/2f47c7d5fcfa98a62b705bf9a0505d86f4563c0d81cab1f7159ff1e743b7/greenlet-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", size = 1625684, upload-time = "2026-06-17T18:22:17.664Z" }, - { url = "https://files.pythonhosted.org/packages/b9/bf/661dd24624f70b7b32972d7693d0344ecde10278f647d7b828baf739899c/greenlet-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", size = 1688043, upload-time = "2026-06-17T17:40:12.403Z" }, - { url = "https://files.pythonhosted.org/packages/60/49/d9bde1d15a21296b3b521fe083eb8aabd54ac05d15de9832918f3d639543/greenlet-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", size = 240531, upload-time = "2026-06-17T17:35:47.448Z" }, - { url = "https://files.pythonhosted.org/packages/7f/4d/86d7768bd53e9907de0333df215c2018cd01a593b3715cbd79aa82dd94b7/greenlet-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:7a7bfc200be40d04961d7e80e8337d726c0c1a50777e588123c3ed8ba731dcb9", size = 239579, upload-time = "2026-06-17T17:39:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/92/15/907be5e8900901039bae752fa9a31c03a3c1e064833f35a4e49449184581/greenlet-3.5.2-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", size = 296697, upload-time = "2026-06-17T17:37:15.887Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/08c57be575c3d6a3c023bbf22144a1c7dc6ed4d134527bb36ded4dbf04a8/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", size = 656710, upload-time = "2026-06-17T18:07:28.046Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/749f917bdc9fc90fceea4aa65fbf6556e617a50714d1496bdc8ad190bb36/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c", size = 662629, upload-time = "2026-06-17T18:29:51.728Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a5/68cefae3a07f6d0093a490cf28ab604f14578f3e60205a2a2b2d5cd70af2/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", size = 660147, upload-time = "2026-06-17T17:39:35.068Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6b/b9156d8397e4750220f54c7c5c34650f1e740a8d2f66eab9cfd1b7b53b69/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", size = 1621675, upload-time = "2026-06-17T18:22:18.873Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e3/d3250f4fa01c211a93d04e34fded63187e648dbec17b9b1a14d388040593/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", size = 1680577, upload-time = "2026-06-17T17:40:14.055Z" }, - { url = "https://files.pythonhosted.org/packages/55/ba/eaee8bda4419770d7096b5a009ebff0ab20a2a28cdd83c4b591bfdf36fa9/greenlet-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", size = 243482, upload-time = "2026-06-17T17:37:34.741Z" }, - { url = "https://files.pythonhosted.org/packages/37/45/f794a81c91e9942c61f9110bd1f9a38a0ea565eab57f8b08cd53d3131e48/greenlet-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:db548d5ab6c2a8ead82c013f875090d79b5d7d2b67fc513934ce6cf66492ad7f", size = 242062, upload-time = "2026-06-17T17:35:39.814Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dd/8b/befc3cb36965f397d87e86fb3b00e3ec0dc67c1ecb0986d7f54ee528f018/greenlet-3.5.2.tar.gz", hash = "sha256:c1b906220d83c140361cdd12eef970fb5881a168b98ee58a43786426173da14c", size = 199243, upload-time = "2026-06-17T20:19:01.317Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2b/3a/cd99db55dc908568f6b91845747b98b3b17a06052fa1803d091dc91da27d/greenlet-3.5.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9df9daae96848508450011d0d86ed7c95f8829a354ce438284a77b24896fd1f8", size = 285626, upload-time = "2026-06-17T17:33:33.231Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ce/09/fd997a19cbb97641233c7d5f8fc89314c132be2c8867c4f14beff979996f/greenlet-3.5.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01e32e9d2b1714a2b06184cb3071ff2a2fd9bc7d065e39198ab21f7253dad421", size = 601821, upload-time = "2026-06-17T18:07:16.756Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7d/b0/62abd204addd913ad9856e091f5d8baaedc7c85df151f22f093b8a207c20/greenlet-3.5.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0488ca77c94da5e09d1d9958f98b58cebba1b8fd9664c24898499133de927574", size = 615044, upload-time = "2026-06-17T18:29:39.344Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/34/67/ceaab731b51611a8238b0af2d4abb4fd727ec09b16cd499fca5295603f46/greenlet-3.5.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d9e19257794e28821c9ebd5e23f86d7c267cd9d390089374f068d2049f949e3", size = 615176, upload-time = "2026-06-17T17:39:25.134Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1c/40/51a0ee73b72a7e4a65b54433316bbd7b3b7902a585310cd4e3051d411ee3/greenlet-3.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bf493b3c1c0a2324c49b0472e2280ba4665f3510d8115f6f807759a6163b15f7", size = 1574580, upload-time = "2026-06-17T18:22:09.082Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/41/d3/a3a2163b1fe73042d3e72cfcb9920f2481d5188a1df2645587a9b83a903f/greenlet-3.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:561dd919c02236a613fbf226791cbd77ee5002cbd5cb7e838869aa3ac7a71e16", size = 1641192, upload-time = "2026-06-17T17:40:04.234Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/95/a3/b4d83fb451e2f7266cb45ccef23857f8a800e0a5d9a73263fafdf7ba7904/greenlet-3.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:049827baab63dda8ab8ec5a6d07fc6eb0f418319cfc757fc8737a605e99ca1ad", size = 238247, upload-time = "2026-06-17T17:34:54.794Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/21/68/371ee6dad168be3386c46030bedaa8e3e7e3cf3d203621d4529e78ff36ef/greenlet-3.5.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d7792398872f89466c6671d5d193537eff163ecf7fac78d82e6ddc25017fb4f5", size = 286925, upload-time = "2026-06-17T17:33:17.928Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/26/16/ed5706c26b4d26f3fabceb79abca992654eac8b0fa435def2ac6dbd92122/greenlet-3.5.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:711028c953cd6ce5dc01bbb5a1747e3ad6bd8b2f7ded73778bb936e8dab9e3b6", size = 606036, upload-time = "2026-06-17T18:07:18.538Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8e/32/f9c77093af9f5f96615922b7e3fe3690a9faff02adb89f1d74e21578b147/greenlet-3.5.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5eba55076d79e8a5176e6925295cfb901ebc95dae493342ede22230f75d8bee2", size = 617821, upload-time = "2026-06-17T18:29:41.317Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/d4/642833e778c17d32b5cabb793e14ce7364c55952462fc506fecdee55d485/greenlet-3.5.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1c1e5ad80f1f38ea479b83b39dccb20874cfe9ad5e52f87225fa294ba4d39a1", size = 616877, upload-time = "2026-06-17T17:39:26.564Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d3/cc/7120f83e78b8be3cf7acbe2306b3b7bd2cbf99f5ad12e85e2f05d7b31961/greenlet-3.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e194b996aa1b89d933cfe136e5eb39b22a8b72ba59d376ef39a55bca4dbf47f", size = 1577274, upload-time = "2026-06-17T18:22:10.692Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fa/d8/05a0074ee485dd51c320fd706fd7ed48006b9cad3443092d7df1a655f0d2/greenlet-3.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e554809538bd4867f24421b43abde170f9c9b8192149b30df5e164bcac6124f", size = 1643566, upload-time = "2026-06-17T17:40:05.452Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/35/fe/9fe2060bdeece682e38d381184ae66045b48ed183c107ab3f88b9886a630/greenlet-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:e063263ce9047878480d7e536012fc8b7c8e1922989eb5f03b9ab998a2ee7b7e", size = 238643, upload-time = "2026-06-17T17:37:03.039Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/41/13/a9db72f5b6b700977ebd371d6a1f2984a08838357de924fcd5571607b1bf/greenlet-3.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:a3f76a94e2d6e1fee8f302265679d8cc47d71a203936dd03c6e2ace0f9cfd46d", size = 237135, upload-time = "2026-06-17T17:34:34.14Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3f/7a/6bc2a7835731387ed303b9390ce68a116ab053df05450a59181239200454/greenlet-3.5.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:76dae33e97b52743a19210931ee3e78a88fe1438bc2fc4ee5e7512d289bfad4f", size = 288351, upload-time = "2026-06-17T17:36:17.019Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/1b/bd98062fcef6d0e9d0873ab6f2d029772e6ea342972ae43275bd6177900f/greenlet-3.5.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30252d191d6959df1d040b559a38fc017139606c5ecc2ad00416557c0355d742", size = 604273, upload-time = "2026-06-17T18:07:20.296Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/25/e6/fe392c522bf45d976abe7db2793f6ef4e87b053ebb869deeaae46aeb54da/greenlet-3.5.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1adc23c50f22b0f5979521909a8360ab4a3d3bef8b641ce633a04cf1b1c967ea", size = 616536, upload-time = "2026-06-17T18:29:43.205Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/68/4a/399ff81fa93a19d6a9df394cef0355f082dbc19ad41aba9593cd0ad444e2/greenlet-3.5.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f052fff492c52fdfa99bd3b3c1389a53de37dae76a0562741417f0d018f02b3", size = 613749, upload-time = "2026-06-17T17:39:28.148Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a5/75/f519593f12ad43d08e28c03a95cfe2eeae011707dbc9dab0c4a263ce90f9/greenlet-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:120b77c2a18ebf629c3a7886f68c6d01e065654844ad468f15bb93ace66f2094", size = 1573725, upload-time = "2026-06-17T18:22:12.023Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f1/bc/bc1ea4b0754c6c51bbf9d94677b0b1f7fbda8cbb404e44a896854fc0a940/greenlet-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a850f6224088ef7dcc70f1a545cb6b3d119c35d6dca63b925b9f35da0635cdad", size = 1638132, upload-time = "2026-06-17T17:40:06.971Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/36/c0/f0f5a34247df60de285f75f22e57f14027f4b3c43820981854b5b643ca6d/greenlet-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:89da99ee8345b458ea2f16831dad31c88ddcdec454b48704d569a0b8fb28f146", size = 239393, upload-time = "2026-06-17T17:33:47.09Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/09/17/a8544e165445f30aea67a8d9cf2786d2bb0eb1b0e0d224b4d9bd80e2d587/greenlet-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:ca92411942154023c65851e6077d8ca0d00f19de5fa80bb2c6f196ff6c920ba9", size = 237723, upload-time = "2026-06-17T17:36:47.776Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/3c/bb37b9d40d65b0741a8b040ca5c307034d0a9822994dff5f825c88dd7a6b/greenlet-3.5.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0629377725977252159de1ebd3c6e49c170a63856e585446797bb3d66d4d9c34", size = 287178, upload-time = "2026-06-17T17:35:25.132Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f0/a6/0c5902393f492f8ceb19d0b5cf139284e3a11b333a049739643b1036b6f8/greenlet-3.5.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2ddf9eddc617681108dd071b3feabf3f4a4cd64846254aec4d4ceda098b639a", size = 606900, upload-time = "2026-06-17T18:07:21.692Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d8/7c/42899c31d4b87148ae4e3f87f63e13398824be6241f4dde42ded95768a34/greenlet-3.5.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f41feb9f2b59e2e61ac9bea4e344ddd9396bf3cacb2583f73a3595ed7df6f8e7", size = 619265, upload-time = "2026-06-17T18:29:44.837Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d3/52/4ff8c98d3cfe62b4515f8584ae14510a58f35c549cc5292b78d9b7a40b70/greenlet-3.5.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09201fa698768db245920b00fdc86ee3e73540f01ca6db162be9632642e1a473", size = 616187, upload-time = "2026-06-17T17:39:29.473Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c9/a6/269c8bf9aefc13361ce1088f0e392b154cb21005de7862e42b5d782b81fd/greenlet-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1759fa4f14c398508cf20dc8037de55cc23ae8bd14c185c2718257837195ca5", size = 1573778, upload-time = "2026-06-17T18:22:13.497Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1f/9b/391d015cbc6323e81b14c02cf825fdca7e0049c9bb489bf4ac72883118ba/greenlet-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9318cdeb9abdbfdd8bc8464ee4a06dffde2c7846e1def138365a6240ab2c9a5", size = 1638092, upload-time = "2026-06-17T17:40:08.163Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/49/53/5b4df711f4356c62e85d9f819d87966d526d1cfb32bae49a8f7d6fc36ea4/greenlet-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:2c3b3311af72b3d3b03cc0f1ffd11f072e834be5d0444105cf715fc44434e39c", size = 239352, upload-time = "2026-06-17T17:38:51.593Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bb/b6/18efc3a329ec035c3f344b8f2b60356451950ddf9b7b64ff00023778a1dd/greenlet-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:f9bbd6216c45a563c2a61e478e038b439d9f248bde44f775ea37d339da643af4", size = 237635, upload-time = "2026-06-17T17:35:36.632Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c7/89/aaafc8e14de4ac882e02ccb963225329b0e8578aba4365e71eb678e45722/greenlet-3.5.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", size = 287676, upload-time = "2026-06-17T17:33:31.514Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/fc/2308249206c12ac70de7b9a00970f84f07d10b3cd60e05d2fbcaa84124e8/greenlet-3.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", size = 653552, upload-time = "2026-06-17T18:07:23.493Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7c/24/47730d1f8f1336b9b089237521ed7a26eee997065dcb4cab81cdca333abc/greenlet-3.5.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", size = 665756, upload-time = "2026-06-17T18:29:46.616Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/99/69/d6c99db15dc0b5e892ac3cc7b942c8b21f4a9cc3bd9ea0bc3b0f339ffbd4/greenlet-3.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", size = 663228, upload-time = "2026-06-17T17:39:31.073Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4f/88/9e603f448e2bc107c883e95817b980fb9b45ba6aea0299b2e9978124bea2/greenlet-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", size = 1620723, upload-time = "2026-06-17T18:22:14.817Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/11/91/26da17e3777858c16fdb8d020a4c68f3a03cb92f238de8f5351d5d5186e9/greenlet-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", size = 1684227, upload-time = "2026-06-17T17:40:09.536Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2d/44/b3a11f7aa34cb38f1b7f3df8bcd9fcd09bac9d342c2a2c9b8686c804bcd2/greenlet-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", size = 240257, upload-time = "2026-06-17T17:35:23.359Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/de/e3/3b62145fe917311732041a258adb218248add00542e3131c48bd047fbed5/greenlet-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3c417cd6c593bbbef6f7aa31a79f37d3db7d18832fc56b694a2150130bde784e", size = 239038, upload-time = "2026-06-17T17:37:56.792Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/47/ac/d3bad483e9f6cd1848604fdffa32cac25846dd6dfcec0e6f81c790185518/greenlet-3.5.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", size = 295668, upload-time = "2026-06-17T17:36:02.293Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/00/e9/3a7e557b895fd0469b00cd0b2bd498ba950e8bfdf6d7adeecf2c5e4130a6/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", size = 652820, upload-time = "2026-06-17T18:07:24.95Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/67/6225d5c5e4afc04be0fd161eec82e4b72017e8a100d222f25d7b42b0140d/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", size = 658697, upload-time = "2026-06-17T18:29:48.365Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fa/99/6324b8ef916dcaddccb340b304c992ca3f947614ce0f2685d438187300b8/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", size = 656436, upload-time = "2026-06-17T17:39:32.509Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a9/ee/f5bf9daac27c5e1b011965f64b5630a32b415daf7381b312943629e12c2a/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", size = 1617193, upload-time = "2026-06-17T18:22:16.252Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8a/21/b05d5b12715bda92ce27c118d64971d21e9b8f3563ed959a7d271e2d4223/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", size = 1677512, upload-time = "2026-06-17T17:40:10.771Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/97/1b8f1314b868041b327dc1051603e8142b826480cb0ecb8a7b7632aee9c4/greenlet-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", size = 243145, upload-time = "2026-06-17T17:34:37.502Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/36/07/1b5311775e04c718a118c504d7a3a312430e2a1bd1347226aff4774e4549/greenlet-3.5.2-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", size = 288315, upload-time = "2026-06-17T17:34:34.04Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/cc/6abcd2a486b58b9f77b7a93b690d59cb2c11a5906ed2ad4c63c7b9c1113d/greenlet-3.5.2-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", size = 659130, upload-time = "2026-06-17T18:07:26.354Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f2/12/f4aaad6d3d383233f700ab322568a4f29f2c701a4861d85f4811d99689b2/greenlet-3.5.2-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", size = 669724, upload-time = "2026-06-17T18:29:50.13Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/91/2a/a089811fc31c6bf8742f40a4e73470d6d401cef18e4314eb20dc399b377c/greenlet-3.5.2-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", size = 668089, upload-time = "2026-06-17T17:39:33.808Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0f/1c/2f47c7d5fcfa98a62b705bf9a0505d86f4563c0d81cab1f7159ff1e743b7/greenlet-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", size = 1625684, upload-time = "2026-06-17T18:22:17.664Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/bf/661dd24624f70b7b32972d7693d0344ecde10278f647d7b828baf739899c/greenlet-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", size = 1688043, upload-time = "2026-06-17T17:40:12.403Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/49/d9bde1d15a21296b3b521fe083eb8aabd54ac05d15de9832918f3d639543/greenlet-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", size = 240531, upload-time = "2026-06-17T17:35:47.448Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7f/4d/86d7768bd53e9907de0333df215c2018cd01a593b3715cbd79aa82dd94b7/greenlet-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:7a7bfc200be40d04961d7e80e8337d726c0c1a50777e588123c3ed8ba731dcb9", size = 239579, upload-time = "2026-06-17T17:39:39.954Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/92/15/907be5e8900901039bae752fa9a31c03a3c1e064833f35a4e49449184581/greenlet-3.5.2-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", size = 296697, upload-time = "2026-06-17T17:37:15.887Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/95/5c/08c57be575c3d6a3c023bbf22144a1c7dc6ed4d134527bb36ded4dbf04a8/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", size = 656710, upload-time = "2026-06-17T18:07:28.046Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8c/d0/749f917bdc9fc90fceea4aa65fbf6556e617a50714d1496bdc8ad190bb36/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c", size = 662629, upload-time = "2026-06-17T18:29:51.728Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5a/a5/68cefae3a07f6d0093a490cf28ab604f14578f3e60205a2a2b2d5cd70af2/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", size = 660147, upload-time = "2026-06-17T17:39:35.068Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d2/6b/b9156d8397e4750220f54c7c5c34650f1e740a8d2f66eab9cfd1b7b53b69/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", size = 1621675, upload-time = "2026-06-17T18:22:18.873Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b0/e3/d3250f4fa01c211a93d04e34fded63187e648dbec17b9b1a14d388040593/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", size = 1680577, upload-time = "2026-06-17T17:40:14.055Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/55/ba/eaee8bda4419770d7096b5a009ebff0ab20a2a28cdd83c4b591bfdf36fa9/greenlet-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", size = 243482, upload-time = "2026-06-17T17:37:34.741Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/37/45/f794a81c91e9942c61f9110bd1f9a38a0ea565eab57f8b08cd53d3131e48/greenlet-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:db548d5ab6c2a8ead82c013f875090d79b5d7d2b67fc513934ce6cf66492ad7f", size = 242062, upload-time = "2026-06-17T17:35:39.814Z" }, ] [[package]] name = "griffelib" version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, ] [[package]] name = "h11" version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "httpcore" version = "1.0.9" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] name = "httpx" version = "0.28.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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 = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } 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" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] [[package]] name = "id" version = "1.6.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/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" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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 = "identify" version = "2.6.19" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, ] [[package]] name = "idna" version = "3.18" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } 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" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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-metadata" version = "8.7.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] [[package]] name = "importlib-resources" version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "ipdb" version = "0.13.13" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "decorator" }, { name = "ipython" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/4c/b075da0092003d9a55cf2ecc1cae9384a1ca4f650d51b00fc59875fe76f6/ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4", size = 12130, upload-time = "2023-03-09T15:40:55.021Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/4c/b075da0092003d9a55cf2ecc1cae9384a1ca4f650d51b00fc59875fe76f6/ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4", size = 12130, upload-time = "2023-03-09T15:40:55.021Z" }, ] [[package]] name = "ipython" version = "8.39.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "decorator" }, @@ -1236,234 +1259,234 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849, upload-time = "2026-03-27T10:02:07.846Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849, upload-time = "2026-03-27T10:02:07.846Z" }, ] [[package]] name = "isoduration" version = "20.11.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "arrow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, ] [[package]] name = "jaraco-classes" version = "3.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, ] [[package]] name = "jaraco-context" version = "6.1.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, ] [[package]] name = "jaraco-functools" version = "4.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/cf/ea4ef2920830dea3f5ab2ea4da6fb67724e6dca80ee2553788c3607243d0/jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03", size = 20272, upload-time = "2026-05-15T21:34:10.025Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/36/cf/ea4ef2920830dea3f5ab2ea4da6fb67724e6dca80ee2553788c3607243d0/jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03", size = 20272, upload-time = "2026-05-15T21:34:10.025Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4", size = 10594, upload-time = "2026-05-15T21:34:08.595Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4", size = 10594, upload-time = "2026-05-15T21:34:08.595Z" }, ] [[package]] name = "jedi" version = "0.20.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, ] [[package]] name = "jeepney" version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] [[package]] name = "jinja2" version = "3.1.6" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "jmespath" version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] [[package]] name = "joblib" version = "1.5.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] [[package]] name = "joserfc" version = "1.7.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/90/25cb27518750218e4f850be63d8bbb2343efaad1c01c3571aaa4b3c33bd7/joserfc-1.7.1.tar.gz", hash = "sha256:77d0b76514879c68c6f433bc5b7357a4ab72008ff1e33d8379fd11d72bd8ca81", size = 233181, upload-time = "2026-06-08T07:21:33.412Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/44/90/25cb27518750218e4f850be63d8bbb2343efaad1c01c3571aaa4b3c33bd7/joserfc-1.7.1.tar.gz", hash = "sha256:77d0b76514879c68c6f433bc5b7357a4ab72008ff1e33d8379fd11d72bd8ca81", size = 233181, upload-time = "2026-06-08T07:21:33.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/00/fa62404c3e347f946faa13aa21085205f9cc06ad17671e37f81a51662ae8/joserfc-1.7.1-py3-none-any.whl", hash = "sha256:b3e3d655612e2e1ef67b2600f2f420e12e537b020208fab1761fad647319c164", size = 70423, upload-time = "2026-06-08T07:21:32.001Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b3/00/fa62404c3e347f946faa13aa21085205f9cc06ad17671e37f81a51662ae8/joserfc-1.7.1-py3-none-any.whl", hash = "sha256:b3e3d655612e2e1ef67b2600f2f420e12e537b020208fab1761fad647319c164", size = 70423, upload-time = "2026-06-08T07:21:32.001Z" }, ] [[package]] name = "jschema-to-python" version = "1.2.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "attrs" }, { name = "jsonpickle" }, { name = "pbr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/7f/5ae3d97ddd86ec33323231d68453afd504041efcfd4f4dde993196606849/jschema_to_python-1.2.3.tar.gz", hash = "sha256:76ff14fe5d304708ccad1284e4b11f96a658949a31ee7faed9e0995279549b91", size = 10061, upload-time = "2019-10-05T20:02:39.657Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1d/7f/5ae3d97ddd86ec33323231d68453afd504041efcfd4f4dde993196606849/jschema_to_python-1.2.3.tar.gz", hash = "sha256:76ff14fe5d304708ccad1284e4b11f96a658949a31ee7faed9e0995279549b91", size = 10061, upload-time = "2019-10-05T20:02:39.657Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/9e/1b6819a87c3f59170406163ba17bc55b0abe18ae552f53d2b0a2025f9c63/jschema_to_python-1.2.3-py3-none-any.whl", hash = "sha256:8a703ca7604d42d74b2815eecf99a33359a8dccbb80806cce386d5e2dd992b05", size = 10400, upload-time = "2019-10-05T20:02:37.948Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/31/9e/1b6819a87c3f59170406163ba17bc55b0abe18ae552f53d2b0a2025f9c63/jschema_to_python-1.2.3-py3-none-any.whl", hash = "sha256:8a703ca7604d42d74b2815eecf99a33359a8dccbb80806cce386d5e2dd992b05", size = 10400, upload-time = "2019-10-05T20:02:37.948Z" }, ] [[package]] name = "jsonpatch" version = "1.33" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "jsonpointer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, ] [[package]] name = "jsonpath-ng" version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/32/58/250751940d75c8019659e15482d548a4aa3b6ce122c515102a4bfdac50e3/jsonpath_ng-1.8.0.tar.gz", hash = "sha256:54252968134b5e549ea5b872f1df1168bd7defe1a52fed5a358c194e1943ddc3", size = 74513, upload-time = "2026-02-24T14:42:06.182Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/32/58/250751940d75c8019659e15482d548a4aa3b6ce122c515102a4bfdac50e3/jsonpath_ng-1.8.0.tar.gz", hash = "sha256:54252968134b5e549ea5b872f1df1168bd7defe1a52fed5a358c194e1943ddc3", size = 74513, upload-time = "2026-02-24T14:42:06.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844, upload-time = "2026-02-28T00:53:19.637Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844, upload-time = "2026-02-28T00:53:19.637Z" }, ] [[package]] name = "jsonpickle" version = "4.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/c0/dde9b4b42cc415b9579573f967f12efbb034e427a2a37e93ad5139891d87/jsonpickle-4.1.2.tar.gz", hash = "sha256:8afed18aa189fd81e2e833b426bb4af485594921f0b1d36c2001fc5637a2f210", size = 319120, upload-time = "2026-05-28T03:50:11.892Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8d/c0/dde9b4b42cc415b9579573f967f12efbb034e427a2a37e93ad5139891d87/jsonpickle-4.1.2.tar.gz", hash = "sha256:8afed18aa189fd81e2e833b426bb4af485594921f0b1d36c2001fc5637a2f210", size = 319120, upload-time = "2026-05-28T03:50:11.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/7b/fd3c7a09649aea9da1d3587aea624d8f9b29963dfd84a1bdb2aa93b36dac/jsonpickle-4.1.2-py3-none-any.whl", hash = "sha256:7ffe34426bc797684dbf1dc84185558bd864cd25b1ff5fb01b7405e392d0a937", size = 47203, upload-time = "2026-05-28T03:50:10.605Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a1/7b/fd3c7a09649aea9da1d3587aea624d8f9b29963dfd84a1bdb2aa93b36dac/jsonpickle-4.1.2-py3-none-any.whl", hash = "sha256:7ffe34426bc797684dbf1dc84185558bd864cd25b1ff5fb01b7405e392d0a937", size = 47203, upload-time = "2026-05-28T03:50:10.605Z" }, ] [[package]] name = "jsonpointer" version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, ] [[package]] name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } +version = "4.25.1" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "attrs" }, { name = "jsonschema-specifications" }, { name = "referencing" }, - { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, ] [package.optional-dependencies] -format = [ +format-nongpl = [ { name = "fqdn" }, { name = "idna" }, { name = "isoduration" }, { name = "jsonpointer" }, { name = "rfc3339-validator" }, - { name = "rfc3987" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, { name = "uri-template" }, { name = "webcolors" }, ] [[package]] name = "jsonschema-path" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } +version = "0.4.6" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ - { name = "attrs" }, { name = "pathable" }, { name = "pyyaml" }, { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/79/cd02a4df6d9270efdc7d3feefe6edd730b0820c39eeaa107a2faee8322d5/jsonschema_path-0.5.0.tar.gz", hash = "sha256:493b156ba895c97602655b620a8456caa2ce08c1aa389f5a7addec065e6e855c", size = 19597, upload-time = "2026-05-19T20:45:00.971Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/01/86/cfee6dd25843bec0760f456599a4f7e7e40221a934b9229fda0662c859bc/jsonschema_path-0.4.6.tar.gz", hash = "sha256:c89eb635f4d497c9ac328eeff359c489755838806a7d033510a692e9576f5c4b", size = 15302, upload-time = "2026-04-27T18:57:08.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/2c/9e69d73c4297508be9e3b64a970ea3971b3eb8db64ffc5802d40bd25981f/jsonschema_path-0.5.0-py3-none-any.whl", hash = "sha256:2790a070bc7abb08ea3dbe4d340ece4efadf639223001f020c7503229ba068e2", size = 24077, upload-time = "2026-05-19T20:44:59.225Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6c/43/3d3065c05a04bb550c143bfbb8e4fd7022cd327e1082bf257bac74923783/jsonschema_path-0.4.6-py3-none-any.whl", hash = "sha256:451354b5311fa955c3144e6e4e255388c751c0121c5570ec5bb9291dd42d08c9", size = 19565, upload-time = "2026-04-27T18:57:06.792Z" }, ] [[package]] name = "jsonschema-specifications" version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] name = "keyring" version = "25.7.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, { name = "jaraco-classes" }, @@ -1473,383 +1496,453 @@ dependencies = [ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, ] [[package]] name = "lazy-object-proxy" version = "1.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" }, - { url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" }, - { url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" }, - { url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" }, - { url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" }, - { url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, - { url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, - { url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" }, - { url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, - { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, - { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, - { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, - { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, - { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, - { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, - { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, - { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, - { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, - { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, - { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, - { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, - { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, - { url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" }, ] [[package]] name = "license-expression" version = "30.4.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "boolean-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, ] [[package]] name = "line-profiler" version = "5.0.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/b6/6d18ad201417a9c5168995541d0fd7981b5652b2b34f6e46a3a93c0f1beb/line_profiler-5.0.2.tar.gz", hash = "sha256:8d8a990c84c64bcde45af22af502d17bc0ae107be405ce41bba92af5c39c0000", size = 407075, upload-time = "2026-02-23T23:31:20.698Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/9c/d2ba5e1f7da98e3dff9c333dd914c284cd733827987e7ed6a039c7fc008c/line_profiler-5.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ab5d8de6d3c0381b477cc73dde9c36b6c52ba1928d6daba85ac9e790a3f0086", size = 651703, upload-time = "2026-02-23T23:29:45.092Z" }, - { url = "https://files.pythonhosted.org/packages/1b/20/9f99d89ff0ad56e5e6190262ce16a8b2dad1f23b9dc0bc4da608fd42c16f/line_profiler-5.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45da5408286b5395ccb707d1cb2b5aeeb8828466cd2f62e8ab2d7cfb0e1b38c", size = 508846, upload-time = "2026-02-23T23:29:47.341Z" }, - { url = "https://files.pythonhosted.org/packages/5a/44/04d4f21dd1ffca9911402a8cc0ded6f9d89dfd5d3f2a0498704502e5b9af/line_profiler-5.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f6163a43474584db9da495d00869d51a66fdef3962ec3df76f998b1a89308123", size = 497428, upload-time = "2026-02-23T23:29:49.008Z" }, - { url = "https://files.pythonhosted.org/packages/bd/61/7e4658db06e3e3c41713bc600fc26e22607b08969d6044dd640ca26613b1/line_profiler-5.0.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7759e9e4688ed1be1e674dee599500ba47f2f2c76f903184df615352bc182a8", size = 1486964, upload-time = "2026-02-23T23:29:50.849Z" }, - { url = "https://files.pythonhosted.org/packages/03/2e/5507cf3190052906ba9fe77477cf655d446a9c517a41c290050e05cd1fec/line_profiler-5.0.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0f8b3b766bde49ca8c1e26b5ff9013358435106d11bc4838764e117d2bcd3ed", size = 1499663, upload-time = "2026-02-23T23:29:53.578Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d9/cb19fb7b899f30d2261bb792d8f9d1e0b6ba5b4f3fc94b45136fd412562a/line_profiler-5.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:42d4bef2ce9f2e2cc6b872034ef4bf2e18ec44f3a8a09bdf91232a74abe4074c", size = 2442445, upload-time = "2026-02-23T23:29:55.952Z" }, - { url = "https://files.pythonhosted.org/packages/83/5d/9b1d142f41ae23b6da22954ec42c367491bcad356c3507f291c101522e88/line_profiler-5.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7dd5942d091541803b38ebc4c9d7f375d43b93e41e811f2449a022fd5b24d283", size = 2525207, upload-time = "2026-02-23T23:29:57.671Z" }, - { url = "https://files.pythonhosted.org/packages/68/a8/d9ac8429b74b1e30e22e2d51bc5d534864dd276ba856ea8ca32fca1f1198/line_profiler-5.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c22d1d8ee371e92149b5d9578e78072bcb36435adb62e84bf3bb0c173e14c6f6", size = 479862, upload-time = "2026-02-23T23:29:59.178Z" }, - { url = "https://files.pythonhosted.org/packages/d1/cd/a92661cb24987d0a4cf86f7ec9f6a0f74ea981c520b6458275d41b11ec0a/line_profiler-5.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:602370a9bb8d020ea28ddac3bb7fd4331c91d495e6e81d5f75752cbb2f2bb802", size = 650547, upload-time = "2026-02-23T23:30:00.593Z" }, - { url = "https://files.pythonhosted.org/packages/7e/77/5489458f8cc01ea00cdf25bc6fa74e748a30e7b758275cc98b485b59de91/line_profiler-5.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26956eef9668f641c9c6f9adb6b1b236252a73405e238452768812af14f9a145", size = 507989, upload-time = "2026-02-23T23:30:01.848Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4f/14aace66e067fb5a774580bc71b348c323c91a4e2ac223a98822de67ecda/line_profiler-5.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64376a726009b7842706a3fef2a6dba051f0bf5a98cbbcafa4c23d6c83bac53c", size = 497137, upload-time = "2026-02-23T23:30:03.515Z" }, - { url = "https://files.pythonhosted.org/packages/52/48/ea92cc96538a192fdf74249f28ad9e9c0526743b12817f920985e7fdbbe2/line_profiler-5.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68432878d7598916f02be3fbb83e3a4727443cfd96af9cbea05cc1ae9749ed82", size = 1526617, upload-time = "2026-02-23T23:30:05.211Z" }, - { url = "https://files.pythonhosted.org/packages/2f/8d/c73544fba5683a50d7579f21614942d9223e2dd618986be56d5beff561e5/line_profiler-5.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7956e79526dc4898ca63dd8e3f435ff674b35d7e06457bfff883efae3ecb8359", size = 1540576, upload-time = "2026-02-23T23:30:07.234Z" }, - { url = "https://files.pythonhosted.org/packages/bf/33/a3255c143148e5886179b689b0413bb0b7edbd6af1531db577f8bf8b69fd/line_profiler-5.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29370b9d239c0e68ea017bbf2b582beed6122a439ef97a8e38228708b52ba595", size = 2480641, upload-time = "2026-02-23T23:30:08.638Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c8/d398438f9af55c4bd799c15c6a9fc5d349c36ef461edce8ed3569768c964/line_profiler-5.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f47682cb1cef2a3b3865e59fdaf2f486196476fa508ddcdd838e3484626c2a68", size = 2565285, upload-time = "2026-02-23T23:30:10.015Z" }, - { url = "https://files.pythonhosted.org/packages/74/08/663f3dd52ebebcb98cddca9ca4f4b51fcd43ce2c8c6b676de28e2ad6f384/line_profiler-5.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:b74416342261d0434e2ae0ff074ec0ecf0ea4e66ec2db5a95dd8b0ec7f2b1a8b", size = 480440, upload-time = "2026-02-23T23:30:11.588Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c4/12ac6f1c139780301d23b9f64ccb160366063124e91134fcccfb24d8b9b7/line_profiler-5.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f0d51ddb054d38a37607b335475c8be9fae4152b01873d1fc1d6b6a317b66398", size = 464965, upload-time = "2026-02-23T23:30:13.262Z" }, - { url = "https://files.pythonhosted.org/packages/99/92/fb766e6355118d2a681c18525d4c005c146ec44b064ccfd70f4529d8d260/line_profiler-5.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:256c1d5e84a93254dbe656d0486322190cc68f6b517544edef17a9f00167e680", size = 646920, upload-time = "2026-02-23T23:30:14.692Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9d/3583c1cdc740206de9e4734bdcf377d649b89ea876bc36001d95b3dea67d/line_profiler-5.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:892f0cd9967b101ce7528be2d388616037c73cb27830effd7493fa021165c622", size = 505695, upload-time = "2026-02-23T23:30:16.375Z" }, - { url = "https://files.pythonhosted.org/packages/27/60/412476a1d09beac783d11d3bbf85fe6c1e3d50058e3c28967fee59c46649/line_profiler-5.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d2d02735843c14337dae3e80d95a732b4657ef759def75162ef97a1aa7466aac", size = 495859, upload-time = "2026-02-23T23:30:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/37/75/65028bad08264fd8f9c3f0fd405c539ff552c2d1cf2a00965157ad148973/line_profiler-5.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d2e166a86dc9c78c349ee18b592b98ebfb9dae615f63fc77cce5f5f751a6ad0", size = 1464882, upload-time = "2026-02-23T23:30:19.273Z" }, - { url = "https://files.pythonhosted.org/packages/0d/6c/2d0286f67e6bb2b00ae23f9af6df18bfc6bb1ac5d803a8f46bd3eb22a8f1/line_profiler-5.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a870b68af1539d718d030f4c4726d35cff4b14ab605147e65222933c5c0e10e", size = 1484331, upload-time = "2026-02-23T23:30:20.571Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a4/b01359733214a1a85c5f86f3953b07deb61b267efa0328e8d436a1ad80ea/line_profiler-5.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fe8cd787caa2a02ca7e138832fa4cab1f198377eaf6e5e8263e8b7506157c454", size = 2411802, upload-time = "2026-02-23T23:30:21.995Z" }, - { url = "https://files.pythonhosted.org/packages/d1/f4/1fa91206a6c50091cf614fdd5c9d349eb3a57d23f5eb8be8fffe7e0525b9/line_profiler-5.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:70ff915ade9e3ec38ff043ff093b590bbb3055e6fc8b311e0fe14cd78fb2a7f7", size = 2495790, upload-time = "2026-02-23T23:30:23.448Z" }, - { url = "https://files.pythonhosted.org/packages/87/18/d389c72dce6c8318c088a7c29ee8961a913c8a1c6469888b517e8f47ddaf/line_profiler-5.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:026779b9dfca0f367174f5d34bcccffce2755db40a4389f0d8a531a2e3ca7cfc", size = 478790, upload-time = "2026-02-23T23:30:24.848Z" }, - { url = "https://files.pythonhosted.org/packages/3f/54/d171600a4190c07215090a88846ef0093b5bf34a81f8059115592dbb1354/line_profiler-5.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:fe22b927f05a61a0149976bf0d22d8e56fa742ec89f3d72358db71a1f440c77b", size = 462269, upload-time = "2026-02-23T23:30:26.237Z" }, - { url = "https://files.pythonhosted.org/packages/a7/64/856b920e026fbd239df875ec05e63583f7bd7f250805215ab6e132da11d1/line_profiler-5.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:016effba91d34d15229d41984e921a27f66a7b634f1d7adf6c57c743f3d6a0eb", size = 642642, upload-time = "2026-02-23T23:30:27.63Z" }, - { url = "https://files.pythonhosted.org/packages/3b/08/0a56fab0a36818af6ffc8073700db2f402db5a62477b69d938c19871d631/line_profiler-5.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:506e800dd408a8aafadf39ff4e4a1375ae7794910d00098f191520a2f390cb99", size = 503787, upload-time = "2026-02-23T23:30:29.226Z" }, - { url = "https://files.pythonhosted.org/packages/ed/9a/0ab45cf92b2c13261b475c440e18bb18d9497cc2ad5dfaf38c231c72b02b/line_profiler-5.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e67f77bcb349a663cb22819f65621bcd2a39889524dd890d1d88f8736841b7b", size = 493631, upload-time = "2026-02-23T23:30:30.502Z" }, - { url = "https://files.pythonhosted.org/packages/fb/15/a5b603f0c7c795aa656a95e2a70d139dc499b5d153b6a3129bbba6b6f913/line_profiler-5.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6b9d08e85fd48d254ae253e76dc72598e94200ef7002eb1ae0bab4cc9c5e41a", size = 1464022, upload-time = "2026-02-23T23:30:31.793Z" }, - { url = "https://files.pythonhosted.org/packages/27/6f/0f399c72eecaf8f8c00e84238b5786afc34d0a4ef5ad10c63c712715ba86/line_profiler-5.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31290e06ac25cd87fee46ebe979541d4ec7c8d6f15c5cbe5874a932b1cee95bb", size = 1483425, upload-time = "2026-02-23T23:30:33.15Z" }, - { url = "https://files.pythonhosted.org/packages/65/18/f4c642a29719a84d17ea8b58cd6e60943573a28228c30c568565ed5512aa/line_profiler-5.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d7fbcc2dbd8534fc6f7d2b440076749b2235cdc525eb177fefafeaf7550373f", size = 2410276, upload-time = "2026-02-23T23:30:34.943Z" }, - { url = "https://files.pythonhosted.org/packages/90/33/701203686e7d27a545e3bbc8e81fffc7d091c42ed33564be4e72376ef45b/line_profiler-5.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55f04671f48afcd90858c18fbdb2509463c77d717ed5424664f096e902206b6b", size = 2495283, upload-time = "2026-02-23T23:30:36.616Z" }, - { url = "https://files.pythonhosted.org/packages/34/e1/59fe065f67ed1fb8f974a9e3434685af1fc1f6a154489f7ab0992eab1c73/line_profiler-5.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:d2262d4bbbcf72bd430fc5763073792a0f1cb20e64de0f7ecf6e8ae16627d876", size = 479287, upload-time = "2026-02-23T23:30:38.152Z" }, - { url = "https://files.pythonhosted.org/packages/e9/83/89f6ae52fa77960404ee88fc078ee680e504bf1ab8724ac01430cee0f5a5/line_profiler-5.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:abf755b020d91b639cbc563015eca381ca64e6bd27ee55ef9004a3a17b6d4dcf", size = 461960, upload-time = "2026-02-23T23:30:39.657Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ae/43caf21edd10a7f5e138bdffcad01ade9a704462a923054402bbadbe5364/line_profiler-5.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a1cc30f3f7877fec826d0f40f400ee6c99239dc6a2f587b8d90d06a42d29c8a5", size = 648335, upload-time = "2026-02-23T23:30:41.042Z" }, - { url = "https://files.pythonhosted.org/packages/34/90/8a1fb985dc582d140fc92608dec3037a484c5f8ab99ae05c24031aa68000/line_profiler-5.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f90923e1cc4ff8eda1d18e525089fca7bfd6dfe8817ec530a913a2c7444ba0fd", size = 508823, upload-time = "2026-02-23T23:30:42.16Z" }, - { url = "https://files.pythonhosted.org/packages/a4/01/855c55e195ac0aadb8ca4e4c65311f945ed02a2491b436bc33cee318d841/line_profiler-5.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cc3d0ecccb14f014d05b32f687d22adcb98bf59fdcc721e7a4330f0372a56f92", size = 499868, upload-time = "2026-02-23T23:30:44.188Z" }, - { url = "https://files.pythonhosted.org/packages/fc/48/fe73d6192a37637534366306a7871ef0f7ff5973bd87da082e4bf5ec0764/line_profiler-5.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5341f36e532e7ed28e323f5502a29b397b66a6708c6427a77f965148a2e5ddec", size = 1460660, upload-time = "2026-02-23T23:30:45.601Z" }, - { url = "https://files.pythonhosted.org/packages/49/1c/e1236e0f3c7ec1e19e74d61ac15143a7826b5767296de87bcf3aa26548a1/line_profiler-5.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cce501f9d996b317b599c0ae99e3eb1bd447874ef8fef1da330b27f3a23eb50", size = 1475222, upload-time = "2026-02-23T23:30:47.014Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ad/02302fd2a82949277036bc557ecebddb9bc6282b76a4da7660258fe82111/line_profiler-5.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b237d82fb792c3db7c80a8675d3c48993d4421b14d96ae602f7fe9ccf1f85903", size = 2413428, upload-time = "2026-02-23T23:30:48.828Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/b3efe646c8b9fdc6fe26720860276c8a2bb745ffe30f5bcbc9726b975673/line_profiler-5.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:74febeca89128a37a32e6500c99665943c0d11e6043f46ce95596d7d1e1732a7", size = 2494741, upload-time = "2026-02-23T23:30:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/0e/ad/ddadd39eb92900f063f27e8f6d748c03dc2638873f07ebf3cee75f29711f/line_profiler-5.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:d6ce98faff60d9552a30e233648a848682b5d664a7e09e9669163a8f01e28147", size = 485700, upload-time = "2026-02-23T23:30:52.373Z" }, - { url = "https://files.pythonhosted.org/packages/d0/45/a529f355eea8fb790fbdee0273d6c0049dba3232a36e82c30d849b00e996/line_profiler-5.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:8be7cc5f4ed9ad87352129d1a494cf5ba7f0fced0472201d83ac9fbfa20f798b", size = 469781, upload-time = "2026-02-23T23:30:53.747Z" }, +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/03/b6/6d18ad201417a9c5168995541d0fd7981b5652b2b34f6e46a3a93c0f1beb/line_profiler-5.0.2.tar.gz", hash = "sha256:8d8a990c84c64bcde45af22af502d17bc0ae107be405ce41bba92af5c39c0000", size = 407075, upload-time = "2026-02-23T23:31:20.698Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/9c/d2ba5e1f7da98e3dff9c333dd914c284cd733827987e7ed6a039c7fc008c/line_profiler-5.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ab5d8de6d3c0381b477cc73dde9c36b6c52ba1928d6daba85ac9e790a3f0086", size = 651703, upload-time = "2026-02-23T23:29:45.092Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1b/20/9f99d89ff0ad56e5e6190262ce16a8b2dad1f23b9dc0bc4da608fd42c16f/line_profiler-5.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45da5408286b5395ccb707d1cb2b5aeeb8828466cd2f62e8ab2d7cfb0e1b38c", size = 508846, upload-time = "2026-02-23T23:29:47.341Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5a/44/04d4f21dd1ffca9911402a8cc0ded6f9d89dfd5d3f2a0498704502e5b9af/line_profiler-5.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f6163a43474584db9da495d00869d51a66fdef3962ec3df76f998b1a89308123", size = 497428, upload-time = "2026-02-23T23:29:49.008Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/61/7e4658db06e3e3c41713bc600fc26e22607b08969d6044dd640ca26613b1/line_profiler-5.0.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7759e9e4688ed1be1e674dee599500ba47f2f2c76f903184df615352bc182a8", size = 1486964, upload-time = "2026-02-23T23:29:50.849Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/03/2e/5507cf3190052906ba9fe77477cf655d446a9c517a41c290050e05cd1fec/line_profiler-5.0.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0f8b3b766bde49ca8c1e26b5ff9013358435106d11bc4838764e117d2bcd3ed", size = 1499663, upload-time = "2026-02-23T23:29:53.578Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c0/d9/cb19fb7b899f30d2261bb792d8f9d1e0b6ba5b4f3fc94b45136fd412562a/line_profiler-5.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:42d4bef2ce9f2e2cc6b872034ef4bf2e18ec44f3a8a09bdf91232a74abe4074c", size = 2442445, upload-time = "2026-02-23T23:29:55.952Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/83/5d/9b1d142f41ae23b6da22954ec42c367491bcad356c3507f291c101522e88/line_profiler-5.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7dd5942d091541803b38ebc4c9d7f375d43b93e41e811f2449a022fd5b24d283", size = 2525207, upload-time = "2026-02-23T23:29:57.671Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/68/a8/d9ac8429b74b1e30e22e2d51bc5d534864dd276ba856ea8ca32fca1f1198/line_profiler-5.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c22d1d8ee371e92149b5d9578e78072bcb36435adb62e84bf3bb0c173e14c6f6", size = 479862, upload-time = "2026-02-23T23:29:59.178Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/cd/a92661cb24987d0a4cf86f7ec9f6a0f74ea981c520b6458275d41b11ec0a/line_profiler-5.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:602370a9bb8d020ea28ddac3bb7fd4331c91d495e6e81d5f75752cbb2f2bb802", size = 650547, upload-time = "2026-02-23T23:30:00.593Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7e/77/5489458f8cc01ea00cdf25bc6fa74e748a30e7b758275cc98b485b59de91/line_profiler-5.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26956eef9668f641c9c6f9adb6b1b236252a73405e238452768812af14f9a145", size = 507989, upload-time = "2026-02-23T23:30:01.848Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cc/4f/14aace66e067fb5a774580bc71b348c323c91a4e2ac223a98822de67ecda/line_profiler-5.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64376a726009b7842706a3fef2a6dba051f0bf5a98cbbcafa4c23d6c83bac53c", size = 497137, upload-time = "2026-02-23T23:30:03.515Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/52/48/ea92cc96538a192fdf74249f28ad9e9c0526743b12817f920985e7fdbbe2/line_profiler-5.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68432878d7598916f02be3fbb83e3a4727443cfd96af9cbea05cc1ae9749ed82", size = 1526617, upload-time = "2026-02-23T23:30:05.211Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2f/8d/c73544fba5683a50d7579f21614942d9223e2dd618986be56d5beff561e5/line_profiler-5.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7956e79526dc4898ca63dd8e3f435ff674b35d7e06457bfff883efae3ecb8359", size = 1540576, upload-time = "2026-02-23T23:30:07.234Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bf/33/a3255c143148e5886179b689b0413bb0b7edbd6af1531db577f8bf8b69fd/line_profiler-5.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29370b9d239c0e68ea017bbf2b582beed6122a439ef97a8e38228708b52ba595", size = 2480641, upload-time = "2026-02-23T23:30:08.638Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fb/c8/d398438f9af55c4bd799c15c6a9fc5d349c36ef461edce8ed3569768c964/line_profiler-5.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f47682cb1cef2a3b3865e59fdaf2f486196476fa508ddcdd838e3484626c2a68", size = 2565285, upload-time = "2026-02-23T23:30:10.015Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/74/08/663f3dd52ebebcb98cddca9ca4f4b51fcd43ce2c8c6b676de28e2ad6f384/line_profiler-5.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:b74416342261d0434e2ae0ff074ec0ecf0ea4e66ec2db5a95dd8b0ec7f2b1a8b", size = 480440, upload-time = "2026-02-23T23:30:11.588Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5a/c4/12ac6f1c139780301d23b9f64ccb160366063124e91134fcccfb24d8b9b7/line_profiler-5.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f0d51ddb054d38a37607b335475c8be9fae4152b01873d1fc1d6b6a317b66398", size = 464965, upload-time = "2026-02-23T23:30:13.262Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/99/92/fb766e6355118d2a681c18525d4c005c146ec44b064ccfd70f4529d8d260/line_profiler-5.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:256c1d5e84a93254dbe656d0486322190cc68f6b517544edef17a9f00167e680", size = 646920, upload-time = "2026-02-23T23:30:14.692Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7c/9d/3583c1cdc740206de9e4734bdcf377d649b89ea876bc36001d95b3dea67d/line_profiler-5.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:892f0cd9967b101ce7528be2d388616037c73cb27830effd7493fa021165c622", size = 505695, upload-time = "2026-02-23T23:30:16.375Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/27/60/412476a1d09beac783d11d3bbf85fe6c1e3d50058e3c28967fee59c46649/line_profiler-5.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d2d02735843c14337dae3e80d95a732b4657ef759def75162ef97a1aa7466aac", size = 495859, upload-time = "2026-02-23T23:30:18.036Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/37/75/65028bad08264fd8f9c3f0fd405c539ff552c2d1cf2a00965157ad148973/line_profiler-5.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d2e166a86dc9c78c349ee18b592b98ebfb9dae615f63fc77cce5f5f751a6ad0", size = 1464882, upload-time = "2026-02-23T23:30:19.273Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0d/6c/2d0286f67e6bb2b00ae23f9af6df18bfc6bb1ac5d803a8f46bd3eb22a8f1/line_profiler-5.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a870b68af1539d718d030f4c4726d35cff4b14ab605147e65222933c5c0e10e", size = 1484331, upload-time = "2026-02-23T23:30:20.571Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4e/a4/b01359733214a1a85c5f86f3953b07deb61b267efa0328e8d436a1ad80ea/line_profiler-5.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fe8cd787caa2a02ca7e138832fa4cab1f198377eaf6e5e8263e8b7506157c454", size = 2411802, upload-time = "2026-02-23T23:30:21.995Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/f4/1fa91206a6c50091cf614fdd5c9d349eb3a57d23f5eb8be8fffe7e0525b9/line_profiler-5.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:70ff915ade9e3ec38ff043ff093b590bbb3055e6fc8b311e0fe14cd78fb2a7f7", size = 2495790, upload-time = "2026-02-23T23:30:23.448Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/87/18/d389c72dce6c8318c088a7c29ee8961a913c8a1c6469888b517e8f47ddaf/line_profiler-5.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:026779b9dfca0f367174f5d34bcccffce2755db40a4389f0d8a531a2e3ca7cfc", size = 478790, upload-time = "2026-02-23T23:30:24.848Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3f/54/d171600a4190c07215090a88846ef0093b5bf34a81f8059115592dbb1354/line_profiler-5.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:fe22b927f05a61a0149976bf0d22d8e56fa742ec89f3d72358db71a1f440c77b", size = 462269, upload-time = "2026-02-23T23:30:26.237Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a7/64/856b920e026fbd239df875ec05e63583f7bd7f250805215ab6e132da11d1/line_profiler-5.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:016effba91d34d15229d41984e921a27f66a7b634f1d7adf6c57c743f3d6a0eb", size = 642642, upload-time = "2026-02-23T23:30:27.63Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3b/08/0a56fab0a36818af6ffc8073700db2f402db5a62477b69d938c19871d631/line_profiler-5.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:506e800dd408a8aafadf39ff4e4a1375ae7794910d00098f191520a2f390cb99", size = 503787, upload-time = "2026-02-23T23:30:29.226Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/9a/0ab45cf92b2c13261b475c440e18bb18d9497cc2ad5dfaf38c231c72b02b/line_profiler-5.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e67f77bcb349a663cb22819f65621bcd2a39889524dd890d1d88f8736841b7b", size = 493631, upload-time = "2026-02-23T23:30:30.502Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fb/15/a5b603f0c7c795aa656a95e2a70d139dc499b5d153b6a3129bbba6b6f913/line_profiler-5.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6b9d08e85fd48d254ae253e76dc72598e94200ef7002eb1ae0bab4cc9c5e41a", size = 1464022, upload-time = "2026-02-23T23:30:31.793Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/27/6f/0f399c72eecaf8f8c00e84238b5786afc34d0a4ef5ad10c63c712715ba86/line_profiler-5.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31290e06ac25cd87fee46ebe979541d4ec7c8d6f15c5cbe5874a932b1cee95bb", size = 1483425, upload-time = "2026-02-23T23:30:33.15Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/65/18/f4c642a29719a84d17ea8b58cd6e60943573a28228c30c568565ed5512aa/line_profiler-5.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d7fbcc2dbd8534fc6f7d2b440076749b2235cdc525eb177fefafeaf7550373f", size = 2410276, upload-time = "2026-02-23T23:30:34.943Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/90/33/701203686e7d27a545e3bbc8e81fffc7d091c42ed33564be4e72376ef45b/line_profiler-5.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55f04671f48afcd90858c18fbdb2509463c77d717ed5424664f096e902206b6b", size = 2495283, upload-time = "2026-02-23T23:30:36.616Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/34/e1/59fe065f67ed1fb8f974a9e3434685af1fc1f6a154489f7ab0992eab1c73/line_profiler-5.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:d2262d4bbbcf72bd430fc5763073792a0f1cb20e64de0f7ecf6e8ae16627d876", size = 479287, upload-time = "2026-02-23T23:30:38.152Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e9/83/89f6ae52fa77960404ee88fc078ee680e504bf1ab8724ac01430cee0f5a5/line_profiler-5.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:abf755b020d91b639cbc563015eca381ca64e6bd27ee55ef9004a3a17b6d4dcf", size = 461960, upload-time = "2026-02-23T23:30:39.657Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c6/ae/43caf21edd10a7f5e138bdffcad01ade9a704462a923054402bbadbe5364/line_profiler-5.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a1cc30f3f7877fec826d0f40f400ee6c99239dc6a2f587b8d90d06a42d29c8a5", size = 648335, upload-time = "2026-02-23T23:30:41.042Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/34/90/8a1fb985dc582d140fc92608dec3037a484c5f8ab99ae05c24031aa68000/line_profiler-5.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f90923e1cc4ff8eda1d18e525089fca7bfd6dfe8817ec530a913a2c7444ba0fd", size = 508823, upload-time = "2026-02-23T23:30:42.16Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a4/01/855c55e195ac0aadb8ca4e4c65311f945ed02a2491b436bc33cee318d841/line_profiler-5.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cc3d0ecccb14f014d05b32f687d22adcb98bf59fdcc721e7a4330f0372a56f92", size = 499868, upload-time = "2026-02-23T23:30:44.188Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fc/48/fe73d6192a37637534366306a7871ef0f7ff5973bd87da082e4bf5ec0764/line_profiler-5.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5341f36e532e7ed28e323f5502a29b397b66a6708c6427a77f965148a2e5ddec", size = 1460660, upload-time = "2026-02-23T23:30:45.601Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/49/1c/e1236e0f3c7ec1e19e74d61ac15143a7826b5767296de87bcf3aa26548a1/line_profiler-5.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cce501f9d996b317b599c0ae99e3eb1bd447874ef8fef1da330b27f3a23eb50", size = 1475222, upload-time = "2026-02-23T23:30:47.014Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8e/ad/02302fd2a82949277036bc557ecebddb9bc6282b76a4da7660258fe82111/line_profiler-5.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b237d82fb792c3db7c80a8675d3c48993d4421b14d96ae602f7fe9ccf1f85903", size = 2413428, upload-time = "2026-02-23T23:30:48.828Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/c7/b3efe646c8b9fdc6fe26720860276c8a2bb745ffe30f5bcbc9726b975673/line_profiler-5.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:74febeca89128a37a32e6500c99665943c0d11e6043f46ce95596d7d1e1732a7", size = 2494741, upload-time = "2026-02-23T23:30:50.755Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0e/ad/ddadd39eb92900f063f27e8f6d748c03dc2638873f07ebf3cee75f29711f/line_profiler-5.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:d6ce98faff60d9552a30e233648a848682b5d664a7e09e9669163a8f01e28147", size = 485700, upload-time = "2026-02-23T23:30:52.373Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/45/a529f355eea8fb790fbdee0273d6c0049dba3232a36e82c30d849b00e996/line_profiler-5.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:8be7cc5f4ed9ad87352129d1a494cf5ba7f0fced0472201d83ac9fbfa20f798b", size = 469781, upload-time = "2026-02-23T23:30:53.747Z" }, ] [[package]] name = "lxml" -version = "5.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/1f/a3b6b74a451ceb84b471caa75c934d2430a4d84395d38ef201d539f38cd1/lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c", size = 8076838, upload-time = "2025-04-23T01:44:29.325Z" }, - { url = "https://files.pythonhosted.org/packages/36/af/a567a55b3e47135b4d1f05a1118c24529104c003f95851374b3748139dc1/lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7", size = 4381827, upload-time = "2025-04-23T01:44:33.345Z" }, - { url = "https://files.pythonhosted.org/packages/50/ba/4ee47d24c675932b3eb5b6de77d0f623c2db6dc466e7a1f199792c5e3e3a/lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf", size = 5204098, upload-time = "2025-04-23T01:44:35.809Z" }, - { url = "https://files.pythonhosted.org/packages/f2/0f/b4db6dfebfefe3abafe360f42a3d471881687fd449a0b86b70f1f2683438/lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28", size = 4930261, upload-time = "2025-04-23T01:44:38.271Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1f/0bb1bae1ce056910f8db81c6aba80fec0e46c98d77c0f59298c70cd362a3/lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609", size = 5529621, upload-time = "2025-04-23T01:44:40.921Z" }, - { url = "https://files.pythonhosted.org/packages/21/f5/e7b66a533fc4a1e7fa63dd22a1ab2ec4d10319b909211181e1ab3e539295/lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4", size = 4983231, upload-time = "2025-04-23T01:44:43.871Z" }, - { url = "https://files.pythonhosted.org/packages/11/39/a38244b669c2d95a6a101a84d3c85ba921fea827e9e5483e93168bf1ccb2/lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7", size = 5084279, upload-time = "2025-04-23T01:44:46.632Z" }, - { url = "https://files.pythonhosted.org/packages/db/64/48cac242347a09a07740d6cee7b7fd4663d5c1abd65f2e3c60420e231b27/lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f", size = 4927405, upload-time = "2025-04-23T01:44:49.843Z" }, - { url = "https://files.pythonhosted.org/packages/98/89/97442835fbb01d80b72374f9594fe44f01817d203fa056e9906128a5d896/lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997", size = 5550169, upload-time = "2025-04-23T01:44:52.791Z" }, - { url = "https://files.pythonhosted.org/packages/f1/97/164ca398ee654eb21f29c6b582685c6c6b9d62d5213abc9b8380278e9c0a/lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c", size = 5062691, upload-time = "2025-04-23T01:44:56.108Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bc/712b96823d7feb53482d2e4f59c090fb18ec7b0d0b476f353b3085893cda/lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b", size = 5133503, upload-time = "2025-04-23T01:44:59.222Z" }, - { url = "https://files.pythonhosted.org/packages/d4/55/a62a39e8f9da2a8b6002603475e3c57c870cd9c95fd4b94d4d9ac9036055/lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b", size = 4999346, upload-time = "2025-04-23T01:45:02.088Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/a393728ae001b92bb1a9e095e570bf71ec7f7fbae7688a4792222e56e5b9/lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563", size = 5627139, upload-time = "2025-04-23T01:45:04.582Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5f/9dcaaad037c3e642a7ea64b479aa082968de46dd67a8293c541742b6c9db/lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5", size = 5465609, upload-time = "2025-04-23T01:45:07.649Z" }, - { url = "https://files.pythonhosted.org/packages/a7/0a/ebcae89edf27e61c45023005171d0ba95cb414ee41c045ae4caf1b8487fd/lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776", size = 5192285, upload-time = "2025-04-23T01:45:10.456Z" }, - { url = "https://files.pythonhosted.org/packages/42/ad/cc8140ca99add7d85c92db8b2354638ed6d5cc0e917b21d36039cb15a238/lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7", size = 3477507, upload-time = "2025-04-23T01:45:12.474Z" }, - { url = "https://files.pythonhosted.org/packages/e9/39/597ce090da1097d2aabd2f9ef42187a6c9c8546d67c419ce61b88b336c85/lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250", size = 3805104, upload-time = "2025-04-23T01:45:15.104Z" }, - { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240, upload-time = "2025-04-23T01:45:18.566Z" }, - { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685, upload-time = "2025-04-23T01:45:21.387Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164, upload-time = "2025-04-23T01:45:23.849Z" }, - { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206, upload-time = "2025-04-23T01:45:26.361Z" }, - { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144, upload-time = "2025-04-23T01:45:28.939Z" }, - { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124, upload-time = "2025-04-23T01:45:31.361Z" }, - { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520, upload-time = "2025-04-23T01:45:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016, upload-time = "2025-04-23T01:45:36.7Z" }, - { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884, upload-time = "2025-04-23T01:45:39.291Z" }, - { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690, upload-time = "2025-04-23T01:45:42.386Z" }, - { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418, upload-time = "2025-04-23T01:45:46.051Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092, upload-time = "2025-04-23T01:45:48.943Z" }, - { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231, upload-time = "2025-04-23T01:45:51.481Z" }, - { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798, upload-time = "2025-04-23T01:45:54.146Z" }, - { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195, upload-time = "2025-04-23T01:45:56.685Z" }, - { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243, upload-time = "2025-04-23T01:45:58.863Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197, upload-time = "2025-04-23T01:46:01.096Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" }, - { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" }, - { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" }, - { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" }, - { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" }, - { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" }, - { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" }, - { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" }, - { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" }, - { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" }, - { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" }, - { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" }, - { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" }, - { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" }, - { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" }, - { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" }, - { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" }, - { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" }, - { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" }, - { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" }, - { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" }, - { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" }, - { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" }, - { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" }, - { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b0/e4d1cbb8c078bc4ae44de9c6a79fec4e2b4151b1b4d50af71d799e76b177/lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55", size = 3892319, upload-time = "2025-04-23T01:49:22.069Z" }, - { url = "https://files.pythonhosted.org/packages/5b/aa/e2bdefba40d815059bcb60b371a36fbfcce970a935370e1b367ba1cc8f74/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740", size = 4211614, upload-time = "2025-04-23T01:49:24.599Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/91ff89d1e092e7cfdd8453a939436ac116db0a665e7f4be0cd8e65c7dc5a/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5", size = 4306273, upload-time = "2025-04-23T01:49:27.355Z" }, - { url = "https://files.pythonhosted.org/packages/be/7c/8c3f15df2ca534589717bfd19d1e3482167801caedfa4d90a575facf68a6/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37", size = 4208552, upload-time = "2025-04-23T01:49:29.949Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d8/9567afb1665f64d73fc54eb904e418d1138d7f011ed00647121b4dd60b38/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571", size = 4331091, upload-time = "2025-04-23T01:49:32.842Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ab/fdbbd91d8d82bf1a723ba88ec3e3d76c022b53c391b0c13cad441cdb8f9e/lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4", size = 3487862, upload-time = "2025-04-23T01:49:36.296Z" }, +version = "6.1.1" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/12/da/dbe4dfc01ac226fb0504fad035f4d69f3202f3502e20e68537631daddd96/lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60", size = 8541124, upload-time = "2026-05-18T19:17:11.589Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/20/f7095ed9fc2c025f9cfe71cc6ec9f1feb05624edc1812423b5f1aecf3d4b/lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d", size = 4602783, upload-time = "2026-05-18T19:17:20.888Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4a/a4/65c63ca98bd129f6cff7b8c2fa48953ab058cc6005b541354e7dd54d8000/lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea", size = 5002687, upload-time = "2026-05-18T19:17:01.738Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/96/1d/ab7a5c4b5a394d98a94e2d0fc67bab8297597426770dd4978370fbdaf531/lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074", size = 5155099, upload-time = "2026-05-18T19:17:05.159Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/b1/07603bfeeb891a2596d5c2a68f7d2f70f7d11c841ebe391412c69c2857b0/lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30", size = 5057225, upload-time = "2026-05-18T19:17:08.117Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7a/16/cb391ee4b90186fa16d9ebcbe3ea96c71b8da3b0686386c8dcbcc3c67d44/lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315", size = 5287643, upload-time = "2026-05-18T19:17:11.507Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/eb/d6/b619717f918fd76747448fdbaee0e769edbc70e659b5b5d0112b7020b7a3/lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1", size = 5412445, upload-time = "2026-05-18T19:17:22.182Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c6/80/12bc5390ac0a3edeb579d9535e5049a5dda663438728e179d52fb319c33a/lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206", size = 4770864, upload-time = "2026-05-18T19:17:26.851Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0b/59/6500c09da3137f54f020e908d81cfc5ee3e8888e908fd380207afad7c2e6/lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067", size = 5359594, upload-time = "2026-05-18T19:17:32.527Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f2/9b/f64b4cc6b7ebcf75d95af3cde934d254b5f2f10d4163928d838d86b6eb48/lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a", size = 5107713, upload-time = "2026-05-18T19:17:04.402Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/16/19/c7388ad5d3a72315d2832dc1458cbf4f2af7f2b990b606ff4876efd04511/lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa", size = 4803973, upload-time = "2026-05-18T19:17:06.545Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3f/22/76197f0bbf165f0b9e75be59be4997e5259cde973f12f098c1b54c7f5d60/lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383", size = 5349925, upload-time = "2026-05-18T19:17:09.743Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/52/d2a0cfeccb9bcdc47c7ee05cdae5d69b48c9acf20997790a6338bb0d0b3b/lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1", size = 5309825, upload-time = "2026-05-18T19:17:13.831Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/4a/b30944266776c2f49749ef2445aa7e78898194134b80ad776386f61b56ae/lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a", size = 3598402, upload-time = "2026-05-18T19:17:08.21Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/97/33691c66a4d7ec1a5a98e7c909a5b83ee45c7f7ba4cf92b1c4cf26e98079/lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5", size = 4021295, upload-time = "2026-05-18T19:17:28.638Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/5f/26a4dd0e12b9456ff7b12a21af5b491eb6629680d1edd73f4140fd386bcf/lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485", size = 3667717, upload-time = "2026-05-19T19:22:44.474Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/62/b0/83f481780d1548750b8ce2ec824073deef2f452d9cd1a6faff8507e3d16d/lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2", size = 8526461, upload-time = "2026-05-18T19:17:25.862Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/d5/30fa0f808002c7329397bfbb24e306789c0b29f04aa5842c07b174b4216f/lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d", size = 4595375, upload-time = "2026-05-18T19:17:34.555Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4f/d2/edb71cf0e561581a7c5eb2626244320eb04e9f8ce6d563184fd668b45073/lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510", size = 4923654, upload-time = "2026-05-18T19:17:42.917Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4c/77/1bc7eeb0de4577d783fb625aa092cc9357883bba35845a3666bf1259f3dc/lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a", size = 5067921, upload-time = "2026-05-18T19:17:49.175Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1b/3c/c0690d74bd2bc17bc03b5b0d093569ead597dd0bfa088bf99eef8c24e19c/lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d", size = 5002456, upload-time = "2026-05-18T19:17:59.715Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/66/8d/d1b3271af0c0f1e27e8472a849e4d2c65bc7766884b9ad2da9e76e145c88/lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8", size = 5202776, upload-time = "2026-05-18T19:18:08.924Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7a/45/689824ffb237fd10125ad273f32b28ff04dc6203c2822c85ff65a93df65e/lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009", size = 5329945, upload-time = "2026-05-18T19:18:13.673Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5d/c0/ef73af53767e958fd87d437c170f272e2f6e6c0f854939f133a895f1e711/lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6", size = 4659237, upload-time = "2026-05-18T19:18:18.657Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a0/5e/e1158e40397585e91cb0472374a1f63d0926a1ddeaa92f13d1a1ffe306d5/lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8", size = 5265904, upload-time = "2026-05-18T19:18:24.883Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a0/16/8687e5d1400ed1c0bc41dace232ebb7553952b618ea1f2e5fb6e2cfbbe23/lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83", size = 5045225, upload-time = "2026-05-18T19:17:20.073Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ca/18/d877bd1ae2e5ffdfd4836565aba350db31feb2f2656d6ce70316ed66a05e/lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6", size = 4712721, upload-time = "2026-05-18T19:17:40.512Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/44/4d/1f44fd1d770b10dacbf6b5c6e520f4d6e0708744930f719dc04e67cab981/lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c", size = 5252549, upload-time = "2026-05-18T19:17:51.236Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/64/5d/1d66b84f850089254c230ef6ea6b267a5a54e2e179a5d960036a05d501d7/lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08", size = 5226877, upload-time = "2026-05-18T19:18:00.875Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ad/00/84c4b5302d42a2d0184f38d538c8a197f33b52a50bd4f7bcfe990bce3036/lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621", size = 3594072, upload-time = "2026-05-18T19:17:12.714Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/61/9d/2e2f7d876349f45e0f3e29f72da311668853d59b58d473a2dea4f0160135/lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28", size = 4025469, upload-time = "2026-05-18T19:17:50.566Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b0/d5/570e6390e4110331e6208b2ba83d1482cc9146808ee118b22824a34c1070/lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b", size = 3667640, upload-time = "2026-05-19T19:22:48.293Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b5/32/86a3f0f724a3a402d4627937a7fc27b160e45e7012b4adf47f6e1e844511/lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e", size = 3930127, upload-time = "2026-05-18T19:19:02.27Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/40/44/d832e82af08723761556d004b1d04d281c09f9a8cecd7d3148548c9941a3/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004", size = 4210769, upload-time = "2026-05-18T19:20:41.427Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6d/39/0dc5949f759ed7d951e0bb8c2f2d9d7aca1908d22352fa84a8afd2ea54af/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e", size = 4318163, upload-time = "2026-05-18T19:20:44.702Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e6/fb/8ab3845fe046ba4cbf74536bcf6801a774b7caf4350de1c5d37f1f0a9e90/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2", size = 4250945, upload-time = "2026-05-18T19:20:47.385Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/68/1b/7553ab136894374ffae8851ec06f98f511cd8e66246e41b6be059d0a7289/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf", size = 4401664, upload-time = "2026-05-18T19:20:50.489Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/db/a4/441aee36c6f6b249823d20fd91f9be9ab89d7c5a8ae542a4a4ca6d342d56/lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84", size = 3508989, upload-time = "2026-05-18T19:18:38.158Z" }, ] [[package]] name = "mando" version = "0.7.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/24/cd70d5ae6d35962be752feccb7dca80b5e0c2d450e995b16abd6275f3296/mando-0.7.1.tar.gz", hash = "sha256:18baa999b4b613faefb00eac4efadcf14f510b59b924b66e08289aa1de8c3500", size = 37868, upload-time = "2022-02-24T08:12:27.316Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/35/24/cd70d5ae6d35962be752feccb7dca80b5e0c2d450e995b16abd6275f3296/mando-0.7.1.tar.gz", hash = "sha256:18baa999b4b613faefb00eac4efadcf14f510b59b924b66e08289aa1de8c3500", size = 37868, upload-time = "2022-02-24T08:12:27.316Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl", hash = "sha256:26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a", size = 28149, upload-time = "2022-02-24T08:12:25.24Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl", hash = "sha256:26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a", size = 28149, upload-time = "2022-02-24T08:12:25.24Z" }, ] [[package]] name = "markdown" version = "3.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] [[package]] name = "markdown-it-py" version = "4.2.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/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" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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 = "markupsafe" version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "marshmallow" version = "4.3.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "backports-datetime-fromisoformat", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/7e/1dbd4096eb7c148cd2841841916f78820bb85a4d80a0c25c02d30815a7fb/marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880", size = 224485, upload-time = "2026-04-03T21:46:32.72Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/25/7e/1dbd4096eb7c148cd2841841916f78820bb85a4d80a0c25c02d30815a7fb/marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880", size = 224485, upload-time = "2026-04-03T21:46:32.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/e0/ff24e25218bb59eb6290a530cea40651b14068b6e3659b20f9c175179632/marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46", size = 49148, upload-time = "2026-04-03T21:46:31.241Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f4/e0/ff24e25218bb59eb6290a530cea40651b14068b6e3659b20f9c175179632/marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46", size = 49148, upload-time = "2026-04-03T21:46:31.241Z" }, ] [[package]] name = "matplotlib-inline" version = "0.2.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, +] + +[[package]] +name = "mcp" +version = "1.23.3" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a7/a4/d06a303f45997e266f2c228081abe299bbcba216cb806128e2e49095d25f/mcp-1.23.3.tar.gz", hash = "sha256:b3b0da2cc949950ce1259c7bfc1b081905a51916fcd7c8182125b85e70825201", size = 600697, upload-time = "2025-12-09T16:04:37.351Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/32/c6/13c1a26b47b3f3a3b480783001ada4268917c9f42d78a079c336da2e75e5/mcp-1.23.3-py3-none-any.whl", hash = "sha256:32768af4b46a1b4f7df34e2bfdf5c6011e7b63d7f1b0e321d0fdef4cd6082031", size = 231570, upload-time = "2025-12-09T16:04:35.56Z" }, ] [[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" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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 = "memory-profiler" version = "0.61.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "psutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/88/e1907e1ca3488f2d9507ca8b0ae1add7b1cd5d3ca2bc8e5b329382ea2c7b/memory_profiler-0.61.0.tar.gz", hash = "sha256:4e5b73d7864a1d1292fb76a03e82a3e78ef934d06828a698d9dada76da2067b0", size = 35935, upload-time = "2022-11-15T17:57:28.994Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b2/88/e1907e1ca3488f2d9507ca8b0ae1add7b1cd5d3ca2bc8e5b329382ea2c7b/memory_profiler-0.61.0.tar.gz", hash = "sha256:4e5b73d7864a1d1292fb76a03e82a3e78ef934d06828a698d9dada76da2067b0", size = 35935, upload-time = "2022-11-15T17:57:28.994Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/26/aaca612a0634ceede20682e692a6c55e35a94c21ba36b807cc40fe910ae1/memory_profiler-0.61.0-py3-none-any.whl", hash = "sha256:400348e61031e3942ad4d4109d18753b2fb08c2f6fb8290671c5513a34182d84", size = 31803, upload-time = "2022-11-15T17:57:27.031Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/49/26/aaca612a0634ceede20682e692a6c55e35a94c21ba36b807cc40fe910ae1/memory_profiler-0.61.0-py3-none-any.whl", hash = "sha256:400348e61031e3942ad4d4109d18753b2fb08c2f6fb8290671c5513a34182d84", size = 31803, upload-time = "2022-11-15T17:57:27.031Z" }, ] [[package]] name = "mergedeep" version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] [[package]] name = "mike" version = "2.2.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "jinja2" }, { name = "mkdocs" }, @@ -1858,15 +1951,15 @@ dependencies = [ { name = "pyyaml-env-tag" }, { name = "verspec" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/47/fa87e9d56bef16cdfe34b059a437e8c6f7ec6f1b9c378871c3cf95ebea9c/mike-2.2.0.tar.gz", hash = "sha256:1e3858e32c0f125aac14432fc7848434358f9ae0962c5c5cde387ad47f6ad25e", size = 38450, upload-time = "2026-04-14T04:59:03.944Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b4/47/fa87e9d56bef16cdfe34b059a437e8c6f7ec6f1b9c378871c3cf95ebea9c/mike-2.2.0.tar.gz", hash = "sha256:1e3858e32c0f125aac14432fc7848434358f9ae0962c5c5cde387ad47f6ad25e", size = 38450, upload-time = "2026-04-14T04:59:03.944Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl", hash = "sha256:e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040", size = 34026, upload-time = "2026-04-14T04:59:02.602Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl", hash = "sha256:e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040", size = 34026, upload-time = "2026-04-14T04:59:02.602Z" }, ] [[package]] name = "mkdocs" version = "1.6.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1882,69 +1975,69 @@ dependencies = [ { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] [[package]] name = "mkdocs-autorefs" version = "1.4.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, ] [[package]] name = "mkdocs-gen-files" version = "0.6.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "mkdocs" }, { name = "properdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/43/428f312149c161cae557eecd35f3c4a82b867998b1d47fb29fdfe927be26/mkdocs_gen_files-0.6.1.tar.gz", hash = "sha256:57d7ff2229e23d077e46d14a33db6d37c8823f6ce1a503c874c1764a71679763", size = 8746, upload-time = "2026-03-16T23:26:09.31Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/43/43/428f312149c161cae557eecd35f3c4a82b867998b1d47fb29fdfe927be26/mkdocs_gen_files-0.6.1.tar.gz", hash = "sha256:57d7ff2229e23d077e46d14a33db6d37c8823f6ce1a503c874c1764a71679763", size = 8746, upload-time = "2026-03-16T23:26:09.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/1b/3075eb67fe66e19db059f0a25744c4e56978a309603a20e1d3353d545b5e/mkdocs_gen_files-0.6.1-py3-none-any.whl", hash = "sha256:b3182bfc6219e35b8d26658cb988368659d5d023aac30c2a819247558fc12189", size = 8282, upload-time = "2026-03-16T23:26:08.292Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ee/1b/3075eb67fe66e19db059f0a25744c4e56978a309603a20e1d3353d545b5e/mkdocs_gen_files-0.6.1-py3-none-any.whl", hash = "sha256:b3182bfc6219e35b8d26658cb988368659d5d023aac30c2a819247558fc12189", size = 8282, upload-time = "2026-03-16T23:26:08.292Z" }, ] [[package]] name = "mkdocs-get-deps" version = "0.2.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, ] [[package]] name = "mkdocs-literate-nav" version = "0.6.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "mkdocs" }, { name = "properdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/af/dd3776a7a713f798f79bec7eb9c661d5cfb83ddc17d9a3667595e53e1559/mkdocs_literate_nav-0.6.3.tar.gz", hash = "sha256:edbaca22343f861fe4e34aac47d55a0c9955c640dbf02eea99fe631e914cf9ee", size = 17526, upload-time = "2026-03-16T23:26:50.688Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/01/af/dd3776a7a713f798f79bec7eb9c661d5cfb83ddc17d9a3667595e53e1559/mkdocs_literate_nav-0.6.3.tar.gz", hash = "sha256:edbaca22343f861fe4e34aac47d55a0c9955c640dbf02eea99fe631e914cf9ee", size = 17526, upload-time = "2026-03-16T23:26:50.688Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/2c/bcf1ae903975ad6f169abb05c1eb0f94395478364deb89270cf034081b29/mkdocs_literate_nav-0.6.3-py3-none-any.whl", hash = "sha256:2c421561280fa9184f88cbf399bebbd4cc17ee507e978a31ce11fd6f3aabf233", size = 13355, upload-time = "2026-03-16T23:26:49.562Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4e/2c/bcf1ae903975ad6f169abb05c1eb0f94395478364deb89270cf034081b29/mkdocs_literate_nav-0.6.3-py3-none-any.whl", hash = "sha256:2c421561280fa9184f88cbf399bebbd4cc17ee507e978a31ce11fd6f3aabf233", size = 13355, upload-time = "2026-03-16T23:26:49.562Z" }, ] [[package]] name = "mkdocs-material" version = "9.7.6" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "babel" }, { name = "backrefs" }, @@ -1958,37 +2051,37 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, ] [[package]] name = "mkdocs-material-extensions" version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] [[package]] name = "mkdocs-section-index" version = "0.3.12" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "mkdocs" }, { name = "properdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/e2/64d0f3f054ca8efe61e706006ff5f0d49ad99620c62c2e04818573391c33/mkdocs_section_index-0.3.12.tar.gz", hash = "sha256:285635bf86c643b0fc7a343053d7a818049817bff4408f52b80c4367bd5e7268", size = 14946, upload-time = "2026-04-16T19:20:00.953Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f1/e2/64d0f3f054ca8efe61e706006ff5f0d49ad99620c62c2e04818573391c33/mkdocs_section_index-0.3.12.tar.gz", hash = "sha256:285635bf86c643b0fc7a343053d7a818049817bff4408f52b80c4367bd5e7268", size = 14946, upload-time = "2026-04-16T19:20:00.953Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/4d/a330cab5e055d45e924cec69da54a3d8ed37643964f8d1fa1a772b496273/mkdocs_section_index-0.3.12-py3-none-any.whl", hash = "sha256:a1100039546beb4ebef63ce6fc91f3195fb9c0c3763105d4d3d7cd31e0a046eb", size = 8932, upload-time = "2026-04-16T19:19:59.741Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b0/4d/a330cab5e055d45e924cec69da54a3d8ed37643964f8d1fa1a772b496273/mkdocs_section_index-0.3.12-py3-none-any.whl", hash = "sha256:a1100039546beb4ebef63ce6fc91f3195fb9c0c3763105d4d3d7cd31e0a046eb", size = 8932, upload-time = "2026-04-16T19:19:59.741Z" }, ] [[package]] name = "mkdocstrings" version = "1.0.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "jinja2" }, { name = "markdown" }, @@ -1997,39 +2090,39 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, ] [[package]] name = "mkdocstrings-python" version = "2.0.5" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" }, ] [[package]] name = "more-itertools" version = "11.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] [[package]] name = "moto" version = "5.2.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "boto3" }, { name = "botocore" }, @@ -2039,9 +2132,9 @@ dependencies = [ { name = "werkzeug" }, { name = "xmltodict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/63/d944f387582cc53f53febbff2b3fa36a6d2ed7c1feef8990bf646cfa9cba/moto-5.2.2.tar.gz", hash = "sha256:aac8023a429e125e91c91f8f4730a67b54f518cda587352f7e67252fe3168f75", size = 8678761, upload-time = "2026-06-06T18:57:54.931Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/47/63/d944f387582cc53f53febbff2b3fa36a6d2ed7c1feef8990bf646cfa9cba/moto-5.2.2.tar.gz", hash = "sha256:aac8023a429e125e91c91f8f4730a67b54f518cda587352f7e67252fe3168f75", size = 8678761, upload-time = "2026-06-06T18:57:54.931Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/45/13cff46f4f617a6e97e1d497d75abd913e250bb4c823a4985668c6e593e4/moto-5.2.2-py3-none-any.whl", hash = "sha256:3817f1e39721ca833579b921e53e3b68547ace6a34d848c9486fbb5905808de9", size = 6698689, upload-time = "2026-06-06T18:57:51.435Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/45/13cff46f4f617a6e97e1d497d75abd913e250bb4c823a4985668c6e593e4/moto-5.2.2-py3-none-any.whl", hash = "sha256:3817f1e39721ca833579b921e53e3b68547ace6a34d848c9486fbb5905808de9", size = 6698689, upload-time = "2026-06-06T18:57:51.435Z" }, ] [package.optional-dependencies] @@ -2061,185 +2154,192 @@ all = [ { name = "pyyaml" }, { name = "setuptools" }, ] +dynamodb = [ + { name = "docker" }, + { name = "py-partiql-parser" }, +] +ssm = [ + { name = "pyyaml" }, +] [[package]] name = "mpmath" version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] [[package]] name = "msgpack" version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" }, - { url = "https://files.pythonhosted.org/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" }, - { url = "https://files.pythonhosted.org/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" }, - { url = "https://files.pythonhosted.org/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" }, - { url = "https://files.pythonhosted.org/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" }, - { url = "https://files.pythonhosted.org/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" }, - { url = "https://files.pythonhosted.org/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" }, - { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, - { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, - { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, - { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, - { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, - { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, - { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, - { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, - { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, - { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, - { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, - { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, - { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, - { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, - { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, - { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, - { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, - { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, - { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, - { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, - { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, - { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, - { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, - { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, - { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, - { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, - { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, - { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, - { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, - { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, - { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, - { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, - { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, - { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, - { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, - { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, - { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, - { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, ] [[package]] name = "multipart" version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/d6/9c4f366d6f9bb8f8fb5eae3acac471335c39510c42b537fd515213d7d8c3/multipart-1.3.1.tar.gz", hash = "sha256:211d7cfc1a7a43e75c4d24ee0e8e0f4f61d522f1a21575303ae85333dea687bf", size = 38929, upload-time = "2026-02-27T10:17:13.7Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8e/d6/9c4f366d6f9bb8f8fb5eae3acac471335c39510c42b537fd515213d7d8c3/multipart-1.3.1.tar.gz", hash = "sha256:211d7cfc1a7a43e75c4d24ee0e8e0f4f61d522f1a21575303ae85333dea687bf", size = 38929, upload-time = "2026-02-27T10:17:13.7Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/ed/e1f03200ee1f0bf4a2b9b72709afefbf5319b68df654e0b84b35c65613ee/multipart-1.3.1-py3-none-any.whl", hash = "sha256:a82b59e1befe74d3d30b3d3f70efd5a2eba4d938f845dcff9faace968888ff29", size = 15061, upload-time = "2026-02-27T10:17:11.943Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/ed/e1f03200ee1f0bf4a2b9b72709afefbf5319b68df654e0b84b35c65613ee/multipart-1.3.1-py3-none-any.whl", hash = "sha256:a82b59e1befe74d3d30b3d3f70efd5a2eba4d938f845dcff9faace968888ff29", size = 15061, upload-time = "2026-02-27T10:17:11.943Z" }, ] [[package]] name = "networkx" version = "3.4.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } resolution-markers = [ "python_full_version < '3.11'", ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, ] [[package]] name = "networkx" version = "3.6.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] [[package]] name = "nh3" version = "0.3.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/1b/ef84624f14954d270f74060a19fc550dd4f06656399447569afb584d8c06/nh3-0.3.6.tar.gz", hash = "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7", size = 24684, upload-time = "2026-06-22T00:47:02.008Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/3e/6506aa4f23dc7b7993a2d0a45dca3ce864ec48380adfe15a173e643c63e8/nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2", size = 1421679, upload-time = "2026-06-22T00:46:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e1/e96e7864a7a53bd6b6fab7e9632467382a2a2c1f3fed951918ad131542fb/nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d", size = 792570, upload-time = "2026-06-22T00:46:22.179Z" }, - { url = "https://files.pythonhosted.org/packages/59/62/5b6108bedaef2b2637fed04c87bdbcb5967b9961758b41f0e466ef22a022/nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e", size = 842243, upload-time = "2026-06-22T00:46:23.801Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4a/526f199626bfcb496bc01a268051b44737962005553b158e985ed7e64865/nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917", size = 1001468, upload-time = "2026-06-22T00:46:25.481Z" }, - { url = "https://files.pythonhosted.org/packages/49/09/0d8e3101636d9ad88cdefb2914e764cb8e876ebdbb4286bfc251277d9c67/nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4", size = 1082933, upload-time = "2026-06-22T00:46:27.15Z" }, - { url = "https://files.pythonhosted.org/packages/09/a1/ea83abe738a3fbaa203dfdb836ca7cbab0e7e9609faaee4fe1d4652599c0/nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9", size = 1043120, upload-time = "2026-06-22T00:46:28.89Z" }, - { url = "https://files.pythonhosted.org/packages/66/69/0654482b8635012fbae67826bd6c381abb05d841ac7388b9b4666300fdad/nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6", size = 1023824, upload-time = "2026-06-22T00:46:30.453Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a6/1f7285ffadc8307c4dbeb08d21b920536d5117785056d1079e998c4dfa44/nh3-0.3.6-cp314-cp314t-win32.whl", hash = "sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796", size = 599253, upload-time = "2026-06-22T00:46:32.072Z" }, - { url = "https://files.pythonhosted.org/packages/36/ea/5542f3c45da4c00290d9d67a65e996702e23e613c4b627de3e09cb9fe357/nh3-0.3.6-cp314-cp314t-win_amd64.whl", hash = "sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6", size = 612553, upload-time = "2026-06-22T00:46:33.53Z" }, - { url = "https://files.pythonhosted.org/packages/66/35/26bd47e6af5915a628281dccdac354ddf4e32f7397047894270acd8c9870/nh3-0.3.6-cp314-cp314t-win_arm64.whl", hash = "sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41", size = 595151, upload-time = "2026-06-22T00:46:34.878Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25", size = 1443564, upload-time = "2026-06-22T00:46:36.66Z" }, - { url = "https://files.pythonhosted.org/packages/41/21/e1084ab18eb589506335c7c7576f2d4643e9a0c0e33983ef0e549a256b96/nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69", size = 838002, upload-time = "2026-06-22T00:46:38.101Z" }, - { url = "https://files.pythonhosted.org/packages/b0/94/f48d08e6f72a406300fa11d8acd929fea1a80d4bf750fa292cb10785f126/nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e", size = 823045, upload-time = "2026-06-22T00:46:39.495Z" }, - { url = "https://files.pythonhosted.org/packages/25/bb/431615ba1d1d3eb63cde0f974f2114edf863a8a3f6049a12fed23fc241d3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae", size = 1093171, upload-time = "2026-06-22T00:46:41.21Z" }, - { url = "https://files.pythonhosted.org/packages/0e/24/a0d80182a18919665fefd19c1c06f1d1df1c9a6455d0252de40c034a0bc3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d", size = 1049217, upload-time = "2026-06-22T00:46:42.804Z" }, - { url = "https://files.pythonhosted.org/packages/0a/13/6f1e302ca674ac74362e150848ad56a1be5145391204f74facdb8e94df12/nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf", size = 917372, upload-time = "2026-06-22T00:46:44.495Z" }, - { url = "https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e", size = 806699, upload-time = "2026-06-22T00:46:45.99Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a6/bfaa00046e58603507dcfc266c4778e3ab7adf68a5dedd73b6274b8d9314/nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec", size = 835165, upload-time = "2026-06-22T00:46:47.617Z" }, - { url = "https://files.pythonhosted.org/packages/30/a8/fb2c38845efb703a9173bffdfc745fc64d2b0e55cfc73a3647d2f028250c/nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0", size = 858282, upload-time = "2026-06-22T00:46:49.276Z" }, - { url = "https://files.pythonhosted.org/packages/68/17/06e72a18ee9b572914447338237ca7eb164c0df901f141bc10d1282247a2/nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78", size = 1014328, upload-time = "2026-06-22T00:46:51.026Z" }, - { url = "https://files.pythonhosted.org/packages/11/f9/3966c61455668c08853bf5e33b4bed93c421f3194ce4de896dc248d6f6ce/nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438", size = 1098207, upload-time = "2026-06-22T00:46:52.674Z" }, - { url = "https://files.pythonhosted.org/packages/19/d3/479cb4ae440424825735d60525b53e3c77fd60fd6e6afc0e984f00eb0178/nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56", size = 1056961, upload-time = "2026-06-22T00:46:54.335Z" }, - { url = "https://files.pythonhosted.org/packages/17/0c/6cdb5ee1e127be50dc8391e54bddc1f64e87bf4bfad0c55633320e2e02db/nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f", size = 1033829, upload-time = "2026-06-22T00:46:56.258Z" }, - { url = "https://files.pythonhosted.org/packages/e9/55/9de666ad975d6ccd77d799ea0add55ee2347aa81286ce21b2a97c070746b/nh3-0.3.6-cp38-abi3-win32.whl", hash = "sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da", size = 609081, upload-time = "2026-06-22T00:46:57.665Z" }, - { url = "https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl", hash = "sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10", size = 624461, upload-time = "2026-06-22T00:46:59.163Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl", hash = "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", size = 603060, upload-time = "2026-06-22T00:47:00.596Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5e/1b/ef84624f14954d270f74060a19fc550dd4f06656399447569afb584d8c06/nh3-0.3.6.tar.gz", hash = "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7", size = 24684, upload-time = "2026-06-22T00:47:02.008Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/99/3e/6506aa4f23dc7b7993a2d0a45dca3ce864ec48380adfe15a173e643c63e8/nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2", size = 1421679, upload-time = "2026-06-22T00:46:20.248Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/e1/e96e7864a7a53bd6b6fab7e9632467382a2a2c1f3fed951918ad131542fb/nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d", size = 792570, upload-time = "2026-06-22T00:46:22.179Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/59/62/5b6108bedaef2b2637fed04c87bdbcb5967b9961758b41f0e466ef22a022/nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e", size = 842243, upload-time = "2026-06-22T00:46:23.801Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4b/4a/526f199626bfcb496bc01a268051b44737962005553b158e985ed7e64865/nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917", size = 1001468, upload-time = "2026-06-22T00:46:25.481Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/49/09/0d8e3101636d9ad88cdefb2914e764cb8e876ebdbb4286bfc251277d9c67/nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4", size = 1082933, upload-time = "2026-06-22T00:46:27.15Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/09/a1/ea83abe738a3fbaa203dfdb836ca7cbab0e7e9609faaee4fe1d4652599c0/nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9", size = 1043120, upload-time = "2026-06-22T00:46:28.89Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/66/69/0654482b8635012fbae67826bd6c381abb05d841ac7388b9b4666300fdad/nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6", size = 1023824, upload-time = "2026-06-22T00:46:30.453Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/a6/1f7285ffadc8307c4dbeb08d21b920536d5117785056d1079e998c4dfa44/nh3-0.3.6-cp314-cp314t-win32.whl", hash = "sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796", size = 599253, upload-time = "2026-06-22T00:46:32.072Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/36/ea/5542f3c45da4c00290d9d67a65e996702e23e613c4b627de3e09cb9fe357/nh3-0.3.6-cp314-cp314t-win_amd64.whl", hash = "sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6", size = 612553, upload-time = "2026-06-22T00:46:33.53Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/66/35/26bd47e6af5915a628281dccdac354ddf4e32f7397047894270acd8c9870/nh3-0.3.6-cp314-cp314t-win_arm64.whl", hash = "sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41", size = 595151, upload-time = "2026-06-22T00:46:34.878Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25", size = 1443564, upload-time = "2026-06-22T00:46:36.66Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/41/21/e1084ab18eb589506335c7c7576f2d4643e9a0c0e33983ef0e549a256b96/nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69", size = 838002, upload-time = "2026-06-22T00:46:38.101Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b0/94/f48d08e6f72a406300fa11d8acd929fea1a80d4bf750fa292cb10785f126/nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e", size = 823045, upload-time = "2026-06-22T00:46:39.495Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/25/bb/431615ba1d1d3eb63cde0f974f2114edf863a8a3f6049a12fed23fc241d3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae", size = 1093171, upload-time = "2026-06-22T00:46:41.21Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0e/24/a0d80182a18919665fefd19c1c06f1d1df1c9a6455d0252de40c034a0bc3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d", size = 1049217, upload-time = "2026-06-22T00:46:42.804Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0a/13/6f1e302ca674ac74362e150848ad56a1be5145391204f74facdb8e94df12/nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf", size = 917372, upload-time = "2026-06-22T00:46:44.495Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e", size = 806699, upload-time = "2026-06-22T00:46:45.99Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/a6/bfaa00046e58603507dcfc266c4778e3ab7adf68a5dedd73b6274b8d9314/nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec", size = 835165, upload-time = "2026-06-22T00:46:47.617Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/30/a8/fb2c38845efb703a9173bffdfc745fc64d2b0e55cfc73a3647d2f028250c/nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0", size = 858282, upload-time = "2026-06-22T00:46:49.276Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/68/17/06e72a18ee9b572914447338237ca7eb164c0df901f141bc10d1282247a2/nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78", size = 1014328, upload-time = "2026-06-22T00:46:51.026Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/11/f9/3966c61455668c08853bf5e33b4bed93c421f3194ce4de896dc248d6f6ce/nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438", size = 1098207, upload-time = "2026-06-22T00:46:52.674Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/d3/479cb4ae440424825735d60525b53e3c77fd60fd6e6afc0e984f00eb0178/nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56", size = 1056961, upload-time = "2026-06-22T00:46:54.335Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/17/0c/6cdb5ee1e127be50dc8391e54bddc1f64e87bf4bfad0c55633320e2e02db/nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f", size = 1033829, upload-time = "2026-06-22T00:46:56.258Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e9/55/9de666ad975d6ccd77d799ea0add55ee2347aa81286ce21b2a97c070746b/nh3-0.3.6-cp38-abi3-win32.whl", hash = "sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da", size = 609081, upload-time = "2026-06-22T00:46:57.665Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl", hash = "sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10", size = 624461, upload-time = "2026-06-22T00:46:59.163Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl", hash = "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", size = 603060, upload-time = "2026-06-22T00:47:00.596Z" }, ] [[package]] name = "nltk" version = "3.9.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "click" }, { name = "joblib" }, { name = "regex" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" }, ] [[package]] name = "nodeenv" version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "openapi-schema-validator" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } +version = "0.8.1" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "jsonschema" }, { name = "jsonschema-specifications" }, @@ -2248,15 +2348,15 @@ dependencies = [ { name = "referencing" }, { name = "rfc3339-validator" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/e8/ab3f27dbca54ec645f7fab714b640907d5d36c2ebb07e87eebd30bd5c81b/openapi_schema_validator-0.9.0.tar.gz", hash = "sha256:b72db64315b89d21834cd3ffef37e3e6893bc876327be2d366e8424b1029afd3", size = 24686, upload-time = "2026-04-27T17:31:27.606Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/21/4b/67b24b2b23d96ea862be2cca3632a546f67a22461200831213e80c3c6011/openapi_schema_validator-0.8.1.tar.gz", hash = "sha256:4c57266ce8cbfa37bb4eb4d62cdb7d19356c3a468e3535743c4562863e1790da", size = 23134, upload-time = "2026-03-02T08:46:29.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/c0/5467967d95378b2cfce312e09cbd0c9ab64354a0922379b734f793edd04f/openapi_schema_validator-0.9.0-py3-none-any.whl", hash = "sha256:faa3bbe7c3aa8ca2087ad83f709dc3b7d920283153a570c03e24ea182558aa25", size = 19980, upload-time = "2026-04-27T17:31:25.965Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6f/87/e9f29f463b230d4b47d65e17858c595153a8ca8c1775f16e406aa82d455d/openapi_schema_validator-0.8.1-py3-none-any.whl", hash = "sha256:0f5859794c5bfa433d478dc5ac5e5768d50adc56b14380c8a6fd3a8113e89c9b", size = 19211, upload-time = "2026-03-02T08:46:28.154Z" }, ] [[package]] name = "openapi-spec-validator" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } +version = "0.8.5" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "jsonschema" }, { name = "jsonschema-path" }, @@ -2265,43 +2365,73 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/d2/640b5149cd5688bc0ad1fdbb4df6a2f7b84a093c8d787c27d566132f8b8b/openapi_spec_validator-0.9.0.tar.gz", hash = "sha256:6d648cff6490ebb799dcfe273792f2941c050158854c721f086599d845da78b8", size = 1756839, upload-time = "2026-05-20T09:23:18.871Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/79/3f/aa0c1150627b4e683ae5673486b7d5cf2623a8821601863ee389e430965a/openapi_spec_validator-0.8.5.tar.gz", hash = "sha256:93b04ef5321d5866b2502371123d86333e5c1444f051d323e02525d9e83c7622", size = 1756845, upload-time = "2026-04-24T15:25:21.334Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/d8/321ff889330acca2e3097f3d4f80a40bcc41b6d34d302978ab32c449520b/openapi_spec_validator-0.9.0-py3-none-any.whl", hash = "sha256:222fecffc7714f6d0a6ad62c0e4b66cc2b7dbfafb7b93acfc6c308abbdb51af8", size = 50328, upload-time = "2026-05-20T09:23:17.017Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/64/96/d7dfe1cc0be2df22d7a97ffb0f8bb00b10d92749aa6e64ffa7cc9a041580/openapi_spec_validator-0.8.5-py3-none-any.whl", hash = "sha256:3669106361856934153991e30714616a294865a33f6411a4c25d1dc2d08cfbc2", size = 50334, upload-time = "2026-04-24T15:25:19.65Z" }, ] [[package]] name = "opentelemetry-api" -version = "1.40.0" -source = { registry = "https://pypi.org/simple" } +version = "1.37.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/04/05040d7ce33a907a2a02257e601992f0cdf11c73b33f13c4492bf6c3d6d5/opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7", size = 64923, upload-time = "2025-09-11T10:29:01.662Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732, upload-time = "2025-09-11T10:28:41.826Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.37.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dc/6c/10018cbcc1e6fff23aac67d7fd977c3d692dbe5f9ef9bb4db5c1268726cc/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c", size = 20430, upload-time = "2025-09-11T10:29:03.605Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/08/13/b4ef09837409a777f3c0af2a5b4ba9b7af34872bc43609dda0c209e4060d/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e", size = 18359, upload-time = "2025-09-11T10:28:44.939Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.37.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5d/e3/6e320aeb24f951449e73867e53c55542bebbaf24faeee7623ef677d66736/opentelemetry_exporter_otlp_proto_http-1.37.0.tar.gz", hash = "sha256:e52e8600f1720d6de298419a802108a8f5afa63c96809ff83becb03f874e44ac", size = 17281, upload-time = "2025-09-11T10:29:04.844Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e9/e9/70d74a664d83976556cec395d6bfedd9b85ec1498b778367d5f93e373397/opentelemetry_exporter_otlp_proto_http-1.37.0-py3-none-any.whl", hash = "sha256:54c42b39945a6cc9d9a2a33decb876eabb9547e0dcb49df090122773447f1aef", size = 19576, upload-time = "2025-09-11T10:28:46.726Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.61b0" -source = { registry = "https://pypi.org/simple" } +version = "0.58b0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/37/6bf8e66bfcee5d3c6515b79cb2ee9ad05fe573c20f7ceb288d0e7eeec28c/opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7", size = 32606, upload-time = "2026-03-04T14:20:16.825Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f6/36/7c307d9be8ce4ee7beb86d7f1d31027f2a6a89228240405a858d6e4d64f9/opentelemetry_instrumentation-0.58b0.tar.gz", hash = "sha256:df640f3ac715a3e05af145c18f527f4422c6ab6c467e40bd24d2ad75a00cb705", size = 31549, upload-time = "2025-09-11T11:42:14.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/3e/f6f10f178b6316de67f0dfdbbb699a24fbe8917cf1743c1595fb9dcdd461/opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63", size = 33448, upload-time = "2026-03-04T14:19:02.447Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d4/db/5ff1cd6c5ca1d12ecf1b73be16fbb2a8af2114ee46d4b0e6d4b23f4f4db7/opentelemetry_instrumentation-0.58b0-py3-none-any.whl", hash = "sha256:50f97ac03100676c9f7fc28197f8240c7290ca1baa12da8bfbb9a1de4f34cc45", size = 33019, upload-time = "2025-09-11T11:41:00.624Z" }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.61b0" -source = { registry = "https://pypi.org/simple" } +version = "0.58b0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "asgiref" }, { name = "opentelemetry-api" }, @@ -2309,29 +2439,29 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/3e/143cf5c034e58037307e6a24f06e0dd64b2c49ae60a965fc580027581931/opentelemetry_instrumentation_asgi-0.61b0.tar.gz", hash = "sha256:9d08e127244361dc33976d39dd4ca8f128b5aa5a7ae425208400a80a095019b5", size = 26691, upload-time = "2026-03-04T14:20:21.038Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/e2/03ff707d881d590c7adaed5e9d1979aed7e5e53fc1ed89035e5ed9f304af/opentelemetry_instrumentation_asgi-0.58b0.tar.gz", hash = "sha256:3ccc0c9c1c8c71e8d9da5945c6dcd9c0c8d147839f208536b7042c6dd98e65c9", size = 25116, upload-time = "2025-09-11T11:42:18.437Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/78/154470cf9d741a7487fbb5067357b87386475bbb77948a6707cae982e158/opentelemetry_instrumentation_asgi-0.61b0-py3-none-any.whl", hash = "sha256:e4b3ce6b66074e525e717efff20745434e5efd5d9df6557710856fba356da7a4", size = 16980, upload-time = "2026-03-04T14:19:10.894Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8c/71/a00884c6655387c70070138acbf79a6616ad5d4489680f40708d75b598a7/opentelemetry_instrumentation_asgi-0.58b0-py3-none-any.whl", hash = "sha256:508a6d79e333d648d2afee0e140b6e80eb5d443be183be58e81d9ff88373168a", size = 16798, upload-time = "2025-09-11T11:41:08.105Z" }, ] [[package]] name = "opentelemetry-instrumentation-boto" -version = "0.61b0" -source = { registry = "https://pypi.org/simple" } +version = "0.58b0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/dcf07272aac51758af6c55e0a62f23e645d1f8f54ec41b107f1a3e765ee1/opentelemetry_instrumentation_boto-0.61b0.tar.gz", hash = "sha256:f8066f5b8a32bc0fe98d0416d161cfcf5a4b94f25f351a49c772679a4a5f09d7", size = 9711, upload-time = "2026-03-04T14:20:24.808Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/26/69/764153b11526480568c787cf77c2f0e3d490fe59db4d053bfd1758f639e7/opentelemetry_instrumentation_boto-0.58b0.tar.gz", hash = "sha256:1f9b543c26fae287d7f838edfacbeb0ee18d68da7c9d30cddbc5b5de54e49008", size = 9705, upload-time = "2025-09-11T11:42:22.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/87/bdfa97c692f2cfc99cd80d39d4469c0816f68ec5ef13049cac2da2f2e641/opentelemetry_instrumentation_boto-0.61b0-py3-none-any.whl", hash = "sha256:d6e8fd937fd47d675b9e98eecff872aa60ed688f4bee75f3c372e74c45440218", size = 10160, upload-time = "2026-03-04T14:19:17.111Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/58/b3/8cbb7d337d1dceb48d6b3f9deed09ccdd9d84d89a0f8ac4cc9ba9788a064/opentelemetry_instrumentation_boto-0.58b0-py3-none-any.whl", hash = "sha256:5f9d7f0a5f587803166167269583ef4c7b2f61ff26a557ae039f60203992d253", size = 10156, upload-time = "2025-09-11T11:41:14.72Z" }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.61b0" -source = { registry = "https://pypi.org/simple" } +version = "0.58b0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, @@ -2339,15 +2469,30 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/35/aa727bb6e6ef930dcdc96a617b83748fece57b43c47d83ba8d83fbeca657/opentelemetry_instrumentation_fastapi-0.61b0.tar.gz", hash = "sha256:3a24f35b07c557ae1bbc483bf8412221f25d79a405f8b047de8b670722e2fa9f", size = 24800, upload-time = "2026-03-04T14:20:32.759Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/64/09/4f8fcab834af6b403e5e2d94bdfb2d0835ba8cd1049bcc156995f47b65fb/opentelemetry_instrumentation_fastapi-0.58b0.tar.gz", hash = "sha256:03da470d694116a0a40f4e76319e42f3ff9efc49abf804b2acc2c07f96661497", size = 24598, upload-time = "2025-09-11T11:42:35.325Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/05/acfeb2cccd434242a0a7d0ea29afaf077e04b42b35b485d89aee4e0d9340/opentelemetry_instrumentation_fastapi-0.61b0-py3-none-any.whl", hash = "sha256:a1a844d846540d687d377516b2ff698b51d87c781b59f47c214359c4a241047c", size = 13485, upload-time = "2026-03-04T14:19:30.351Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/45/fb/82de06eba54e5cb979274f073065ebc374794853502d342b5155073d1194/opentelemetry_instrumentation_fastapi-0.58b0-py3-none-any.whl", hash = "sha256:d89bfec69c9ffc5d9f3fe58655d6660a66b2bca863b9132712c06edcde68b6fa", size = 13460, upload-time = "2025-09-11T11:41:28.507Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.58b0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/36/42/83ee32de763b919779aaa595b60c5a7b9c0a4b33952bbe432c5f6a783085/opentelemetry_instrumentation_requests-0.58b0.tar.gz", hash = "sha256:ae9495e6ff64e27bdb839fce91dbb4be56e325139828e8005f875baf41951a2e", size = 15188, upload-time = "2025-09-11T11:42:51.268Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/90/4d/f3476b28ea167d1762134352d01ae9693940a42c78994d9f1b32a4477816/opentelemetry_instrumentation_requests-0.58b0-py3-none-any.whl", hash = "sha256:672a0be0bb5b52bea0c11820b35e27edcf4cd22d34abe4afc59a92a80519f8a8", size = 12966, upload-time = "2025-09-11T11:41:52.67Z" }, ] [[package]] name = "opentelemetry-instrumentation-sqlalchemy" -version = "0.61b0" -source = { registry = "https://pypi.org/simple" } +version = "0.58b0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, @@ -2355,45 +2500,71 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/4f/3a325b180944610697a0a926d49d782b41a86120050d44fefb2715b630ac/opentelemetry_instrumentation_sqlalchemy-0.61b0.tar.gz", hash = "sha256:13a3a159a2043a52f0180b3757fbaa26741b0e08abb50deddce4394c118956e6", size = 15343, upload-time = "2026-03-04T14:20:47.648Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/91/6f/fa2c45d5dfb8da0bcc337a421fd12946994cb5e9782c40a21c669e78460d/opentelemetry_instrumentation_sqlalchemy-0.58b0.tar.gz", hash = "sha256:3e4b444a05088ba473710df9d5c730bb08969c8ea71e04f2886a0f7efee22c12", size = 14993, upload-time = "2025-09-11T11:42:52.053Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/97/b906a930c6a1a20c53ecc8b58cabc2cdd0ce560a2b5d44259084ffe4333e/opentelemetry_instrumentation_sqlalchemy-0.61b0-py3-none-any.whl", hash = "sha256:f115e0be54116ba4c327b8d7b68db4045ee18d44439d888ab8130a549c50d1c1", size = 14547, upload-time = "2026-03-04T14:19:53.088Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/26/7a86285adf7131801fe297bdb756b00bc3f43f78bc7e18668d88f7308e53/opentelemetry_instrumentation_sqlalchemy-0.58b0-py3-none-any.whl", hash = "sha256:7c11d11887b3a4dbc3fccdd58a24903e306052ff7a59685caea1dd7797f82b0a", size = 14212, upload-time = "2025-09-11T11:41:53.76Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-threading" +version = "0.58b0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "wrapt" }, +] +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/70/a9/3888cb0470e6eb48ea17b6802275ae71df411edd6382b9a8e8f391936fda/opentelemetry_instrumentation_threading-0.58b0.tar.gz", hash = "sha256:f68c61f77841f9ff6270176f4d496c10addbceacd782af434d705f83e4504862", size = 8770, upload-time = "2025-09-11T11:42:56.308Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a5/54/add1076cb37980e617723a96e29c84006983e8ad6fc589dde7f69ddc57d4/opentelemetry_instrumentation_threading-0.58b0-py3-none-any.whl", hash = "sha256:eacc072881006aceb5b9b6831bcdce718c67ef6f31ac0b32bd6a23a94d979b4a", size = 9312, upload-time = "2025-09-11T11:41:58.603Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.37.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dd/ea/a75f36b463a36f3c5a10c0b5292c58b31dbdde74f6f905d3d0ab2313987b/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538", size = 46151, upload-time = "2025-09-11T10:29:11.04Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c4/25/f89ea66c59bd7687e218361826c969443c4fa15dfe89733f3bf1e2a9e971/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2", size = 72534, upload-time = "2025-09-11T10:28:56.831Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.40.0" -source = { registry = "https://pypi.org/simple" } +version = "1.37.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f4/62/2e0ca80d7fe94f0b193135375da92c640d15fe81f636658d2acf373086bc/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5", size = 170404, upload-time = "2025-09-11T10:29:11.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9f/62/9f4ad6a54126fb00f7ed4bb5034964c6e4f00fcd5a905e115bd22707e20d/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c", size = 131941, upload-time = "2025-09-11T10:28:57.83Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.61b0" -source = { registry = "https://pypi.org/simple" } +version = "0.58b0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/aa/1b/90701d91e6300d9f2fb352153fb1721ed99ed1f6ea14fa992c756016e63a/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25", size = 129867, upload-time = "2025-09-11T10:29:12.597Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954, upload-time = "2025-09-11T10:28:59.218Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.61b0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/3c/f0196223efc5c4ca19f8fad3d5462b171ac6333013335ce540c01af419e9/opentelemetry_util_http-0.61b0.tar.gz", hash = "sha256:1039cb891334ad2731affdf034d8fb8b48c239af9b6dd295e5fabd07f1c95572", size = 11361, upload-time = "2026-03-04T14:20:57.01Z" } +version = "0.58b0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c6/5f/02f31530faf50ef8a41ab34901c05cbbf8e9d76963ba2fb852b0b4065f4e/opentelemetry_util_http-0.58b0.tar.gz", hash = "sha256:de0154896c3472c6599311c83e0ecee856c4da1b17808d39fdc5cce5312e4d89", size = 9411, upload-time = "2025-09-11T11:43:05.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/e5/c08aaaf2f64288d2b6ef65741d2de5454e64af3e050f34285fb1907492fe/opentelemetry_util_http-0.61b0-py3-none-any.whl", hash = "sha256:8e715e848233e9527ea47e275659ea60a57a75edf5206a3b937e236a6da5fc33", size = 9281, upload-time = "2026-03-04T14:20:08.364Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a5/a3/0a1430c42c6d34d8372a16c104e7408028f0c30270d8f3eb6cccf2e82934/opentelemetry_util_http-0.58b0-py3-none-any.whl", hash = "sha256:6c6b86762ed43025fbd593dc5f700ba0aa3e09711aedc36fd48a13b23d8cb1e7", size = 7652, upload-time = "2025-09-11T11:42:09.682Z" }, ] [[package]] @@ -2417,10 +2588,11 @@ dependencies = [ [package.optional-dependencies] all = [ + { name = "boto3" }, + { name = "botocore" }, { name = "fastapi" }, { name = "jinja2" }, { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation-boto" }, { name = "opentelemetry-instrumentation-fastapi" }, { name = "opentelemetry-instrumentation-sqlalchemy" }, { name = "opentelemetry-sdk" }, @@ -2431,12 +2603,20 @@ all = [ { name = "starlette" }, { name = "uvicorn" }, ] +all-providers = [ + { name = "boto3" }, + { name = "botocore" }, +] api = [ { name = "fastapi" }, { name = "jinja2" }, { name = "starlette" }, { name = "uvicorn" }, ] +aws = [ + { name = "boto3" }, + { name = "botocore" }, +] ci = [ { name = "bandit" }, { name = "bandit-sarif-formatter" }, @@ -2489,6 +2669,8 @@ cli = [ dev = [ { name = "bandit" }, { name = "bandit-sarif-formatter" }, + { name = "boto3" }, + { name = "botocore" }, { name = "build" }, { name = "bump2version" }, { name = "coverage" }, @@ -2546,6 +2728,16 @@ dev = [ { name = "wheel" }, ] monitoring = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-sqlalchemy" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, + { name = "psutil" }, +] +monitoring-aws = [ + { name = "boto3" }, + { name = "botocore" }, { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation-boto" }, { name = "opentelemetry-instrumentation-fastapi" }, @@ -2554,6 +2746,13 @@ monitoring = [ { name = "prometheus-client" }, { name = "psutil" }, ] +test-aws = [ + { name = "boto3" }, + { name = "botocore" }, + { name = "moto", extra = ["dynamodb", "ssm"] }, + { name = "requests-mock" }, + { name = "responses" }, +] [package.dev-dependencies] ci = [ @@ -2566,6 +2765,7 @@ ci = [ { name = "httpx" }, { name = "jinja2" }, { name = "joserfc" }, + { name = "lxml" }, { name = "marshmallow" }, { name = "mike" }, { name = "mkdocs" }, @@ -2616,6 +2816,7 @@ dev = [ { name = "jinja2" }, { name = "joserfc" }, { name = "line-profiler" }, + { name = "lxml" }, { name = "marshmallow" }, { name = "memory-profiler" }, { name = "mike" }, @@ -2666,7 +2867,9 @@ requires-dist = [ { name = "bandit", marker = "extra == 'ci'", specifier = ">=1.7.5,<2.0.0" }, { name = "bandit-sarif-formatter", marker = "extra == 'ci'", specifier = ">=1.1.1,<2.0.0" }, { name = "boto3", specifier = ">=1.42.21" }, + { name = "boto3", marker = "extra == 'aws'", specifier = ">=1.42.21" }, { name = "botocore", specifier = ">=1.42.21" }, + { name = "botocore", marker = "extra == 'aws'", specifier = ">=1.42.21" }, { name = "build", marker = "extra == 'ci'", specifier = ">=1.0.3,<2.0.0" }, { name = "bump2version", marker = "extra == 'dev'", specifier = ">=1.0.1,<2.0.0" }, { name = "coverage", marker = "extra == 'ci'", specifier = ">=7.3.2,<8.0.0" }, @@ -2695,14 +2898,18 @@ requires-dist = [ { name = "mkdocstrings", marker = "extra == 'ci'", specifier = ">=0.22.0,<2.0.0" }, { name = "mkdocstrings-python", marker = "extra == 'ci'", specifier = ">=1.1.0,<3.0.0" }, { name = "moto", extras = ["all"], marker = "extra == 'ci'", specifier = ">=5.1.19,<6.0.0" }, + { name = "moto", extras = ["ec2", "iam", "ec2instanceconnect", "autoscaling", "ssm", "dynamodb", "sts", "sqs", "sns"], marker = "extra == 'test-aws'", specifier = ">=5.1.19,<6.0.0" }, { name = "nltk", marker = "extra == 'ci'", specifier = ">=3.9.3" }, { name = "opentelemetry-api", marker = "extra == 'monitoring'", specifier = ">=1.20.0" }, - { name = "opentelemetry-instrumentation-boto", marker = "extra == 'monitoring'", specifier = ">=0.41b0" }, + { name = "opentelemetry-instrumentation-boto", marker = "extra == 'monitoring-aws'", specifier = ">=0.41b0" }, { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'monitoring'", specifier = ">=0.41b0" }, { name = "opentelemetry-instrumentation-sqlalchemy", marker = "extra == 'monitoring'", specifier = ">=0.41b0" }, { name = "opentelemetry-sdk", marker = "extra == 'monitoring'", specifier = ">=1.20.0" }, - { name = "orb-py", extras = ["ci"], marker = "extra == 'dev'" }, - { name = "orb-py", extras = ["cli", "api", "monitoring"], marker = "extra == 'all'" }, + { name = "orb-py", extras = ["aws"], marker = "extra == 'all-providers'" }, + { name = "orb-py", extras = ["aws"], marker = "extra == 'test-aws'" }, + { name = "orb-py", extras = ["aws", "monitoring"], marker = "extra == 'monitoring-aws'" }, + { name = "orb-py", extras = ["ci", "all-providers"], marker = "extra == 'dev'" }, + { name = "orb-py", extras = ["cli", "api", "monitoring", "all-providers"], marker = "extra == 'all'" }, { name = "pathspec", marker = "extra == 'ci'", specifier = ">=0.11.0" }, { name = "peewee", marker = "extra == 'ci'", specifier = ">=3.18.3" }, { name = "pip-audit", marker = "extra == 'ci'", specifier = ">=2.6.1,<3.0.0" }, @@ -2730,7 +2937,9 @@ requires-dist = [ { name = "radon", marker = "extra == 'dev'", specifier = ">=6.0.1,<7.0.0" }, { name = "requests", specifier = ">=2.31.0" }, { name = "requests-mock", marker = "extra == 'ci'", specifier = ">=1.11.0,<2.0.0" }, + { name = "requests-mock", marker = "extra == 'test-aws'", specifier = ">=1.11.0,<2.0.0" }, { name = "responses", marker = "extra == 'ci'", specifier = ">=0.24.0,<1.0.0" }, + { name = "responses", marker = "extra == 'test-aws'", specifier = ">=0.24.0,<1.0.0" }, { name = "rich", marker = "extra == 'cli'", specifier = ">=13.3.0" }, { name = "rich-argparse", marker = "extra == 'cli'", specifier = ">=1.0.0" }, { name = "ruff", marker = "extra == 'ci'", specifier = ">=0.1.0" }, @@ -2748,7 +2957,7 @@ requires-dist = [ { name = "werkzeug", marker = "extra == 'ci'", specifier = ">=3.1.6" }, { name = "wheel", marker = "extra == 'ci'", specifier = ">=0.41.3,<1.0.0" }, ] -provides-extras = ["cli", "api", "monitoring", "all", "ci", "dev"] +provides-extras = ["aws", "all-providers", "cli", "api", "monitoring", "monitoring-aws", "all", "test-aws", "ci", "dev"] [package.metadata.requires-dev] ci = [ @@ -2761,6 +2970,7 @@ ci = [ { name = "httpx", specifier = ">=0.27.0" }, { name = "jinja2", specifier = ">=3.1.0" }, { name = "joserfc", specifier = ">=1.6.1" }, + { name = "lxml", specifier = ">=6.1.0" }, { name = "marshmallow", specifier = ">=4.1.2" }, { name = "mike", specifier = ">=1.1.0,<3.0.0" }, { name = "mkdocs", specifier = ">=1.5.0,<2.0.0" }, @@ -2811,6 +3021,7 @@ dev = [ { name = "jinja2", specifier = ">=3.1.0" }, { name = "joserfc", specifier = ">=1.6.1" }, { name = "line-profiler", specifier = ">=4.1.1,<6.0.0" }, + { name = "lxml", specifier = ">=6.1.0" }, { name = "marshmallow", specifier = ">=4.1.2" }, { name = "memory-profiler", specifier = ">=0.61.0,<1.0.0" }, { name = "mike", specifier = ">=1.1.0,<3.0.0" }, @@ -2859,115 +3070,115 @@ dev = [ [[package]] name = "packageurl-python" version = "0.17.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" }, ] [[package]] name = "packaging" version = "26.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "paginate" version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] [[package]] name = "parso" version = "0.8.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, ] [[package]] name = "pathable" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/f3/5a20387de9bcd0607871bfc2198ee0e15836da7baa4592ccd7f24c27c986/pathable-0.6.0.tar.gz", hash = "sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58", size = 18970, upload-time = "2026-05-19T18:15:11.911Z" } +version = "0.5.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/e8/6d75ffd9784bce2e93d1ae4415649427e39a53bb172d4672b2b59c6f0a7b/pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566", size = 18983, upload-time = "2026-05-19T18:15:10.728Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, ] [[package]] name = "pathspec" version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] name = "pbr" version = "7.0.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/ab/1de9a4f730edde1bdbbc2b8d19f8fa326f036b4f18b2f72cfbea7dc53c26/pbr-7.0.3.tar.gz", hash = "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29", size = 135625, upload-time = "2025-11-03T17:04:56.274Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5e/ab/1de9a4f730edde1bdbbc2b8d19f8fa326f036b4f18b2f72cfbea7dc53c26/pbr-7.0.3.tar.gz", hash = "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29", size = 135625, upload-time = "2025-11-03T17:04:56.274Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/db/61efa0d08a99f897ef98256b03e563092d36cc38dc4ebe4a85020fe40b31/pbr-7.0.3-py2.py3-none-any.whl", hash = "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b", size = 131898, upload-time = "2025-11-03T17:04:54.875Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c0/db/61efa0d08a99f897ef98256b03e563092d36cc38dc4ebe4a85020fe40b31/pbr-7.0.3-py2.py3-none-any.whl", hash = "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b", size = 131898, upload-time = "2025-11-03T17:04:54.875Z" }, ] [[package]] name = "peewee" version = "3.19.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/b0/79462b42e89764998756e0557f2b58a15610a5b4512fbbcccae58fba7237/peewee-3.19.0.tar.gz", hash = "sha256:f88292a6f0d7b906cb26bca9c8599b8f4d8920ebd36124400d0cbaaaf915511f", size = 974035, upload-time = "2026-01-07T17:24:59.597Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/88/b0/79462b42e89764998756e0557f2b58a15610a5b4512fbbcccae58fba7237/peewee-3.19.0.tar.gz", hash = "sha256:f88292a6f0d7b906cb26bca9c8599b8f4d8920ebd36124400d0cbaaaf915511f", size = 974035, upload-time = "2026-01-07T17:24:59.597Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/41/19c65578ef9a54b3083253c68a607f099642747168fe00f3a2bceb7c3a34/peewee-3.19.0-py3-none-any.whl", hash = "sha256:de220b94766e6008c466e00ce4ba5299b9a832117d9eb36d45d0062f3cfd7417", size = 411885, upload-time = "2026-01-07T17:24:58.33Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1a/41/19c65578ef9a54b3083253c68a607f099642747168fe00f3a2bceb7c3a34/peewee-3.19.0-py3-none-any.whl", hash = "sha256:de220b94766e6008c466e00ce4ba5299b9a832117d9eb36d45d0062f3cfd7417", size = 411885, upload-time = "2026-01-07T17:24:58.33Z" }, ] [[package]] name = "pexpect" version = "4.9.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "ptyprocess" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] [[package]] name = "pip" version = "26.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, ] [[package]] name = "pip-api" version = "0.0.34" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "pip" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" }, ] [[package]] name = "pip-audit" -version = "2.9.0" -source = { registry = "https://pypi.org/simple" } +version = "2.10.1" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "cachecontrol", extra = ["filecache"] }, { name = "cyclonedx-python-lib" }, @@ -2977,30 +3188,31 @@ dependencies = [ { name = "platformdirs" }, { name = "requests" }, { name = "rich" }, - { name = "toml" }, + { name = "tomli" }, + { name = "tomli-w" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/7f/28fad19a9806f796f13192ab6974c07c4a04d9cbb8e30dd895c3c11ce7ee/pip_audit-2.9.0.tar.gz", hash = "sha256:0b998410b58339d7a231e5aa004326a294e4c7c6295289cdc9d5e1ef07b1f44d", size = 52089, upload-time = "2025-04-07T16:45:23.679Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/66/a4/f21d5f0a0edabcbce31560b73c7c5a6f72ae87af4236fd1069c8f59a353d/pip_audit-2.10.1.tar.gz", hash = "sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc", size = 54275, upload-time = "2026-06-10T22:17:01.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/9e/f4dfd9d3dadb6d6dc9406f1111062f871e2e248ed7b584cca6020baf2ac1/pip_audit-2.9.0-py3-none-any.whl", hash = "sha256:348b16e60895749a0839875d7cc27ebd692e1584ebe5d5cb145941c8e25a80bd", size = 58634, upload-time = "2025-04-07T16:45:22.056Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a3/a7/b0c504148114047bd1bc9d97447453c6850ca176bb2f3c0038835994e8b7/pip_audit-2.10.1-py3-none-any.whl", hash = "sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a", size = 62023, upload-time = "2026-06-10T22:17:00.309Z" }, ] [[package]] name = "pip-requirements-parser" version = "32.0.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "packaging" }, { name = "pyparsing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" }, ] [[package]] name = "pip-tools" version = "7.5.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "build" }, { name = "click" }, @@ -3010,33 +3222,33 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/db/c6e2a02db5d98aa5f3250a305ce71e8bc3d1a022d1f47a54d14492ae23de/pip_tools-7.5.3.tar.gz", hash = "sha256:8fa364779ebc010cbfe17cb9de404457ac733e100840423f28f6955de7742d41", size = 176153, upload-time = "2026-02-11T18:25:07.72Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4a/db/c6e2a02db5d98aa5f3250a305ce71e8bc3d1a022d1f47a54d14492ae23de/pip_tools-7.5.3.tar.gz", hash = "sha256:8fa364779ebc010cbfe17cb9de404457ac733e100840423f28f6955de7742d41", size = 176153, upload-time = "2026-02-11T18:25:07.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/74/59906d876c6cb1137f42a137164f2fe683b06283cde84bfcf7f5dd43970b/pip_tools-7.5.3-py3-none-any.whl", hash = "sha256:3aac0c473240ae90db7213c033401f345b05197293ccbdd2704e52e7a783785e", size = 71359, upload-time = "2026-02-11T18:25:06.119Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6e/74/59906d876c6cb1137f42a137164f2fe683b06283cde84bfcf7f5dd43970b/pip_tools-7.5.3-py3-none-any.whl", hash = "sha256:3aac0c473240ae90db7213c033401f345b05197293ccbdd2704e52e7a783785e", size = 71359, upload-time = "2026-02-11T18:25:06.119Z" }, ] [[package]] name = "platformdirs" version = "4.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] name = "pluggy" version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pre-commit" version = "4.6.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "cfgv" }, { name = "identify" }, @@ -3044,36 +3256,36 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] [[package]] name = "prometheus-client" version = "0.25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, ] [[package]] name = "prompt-toolkit" version = "3.0.52" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] [[package]] name = "properdocs" version = "1.6.7" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -3088,325 +3300,345 @@ dependencies = [ { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/29/f27a4e1eddf72ed3db6e47818fbafe6debbf09fd7051f9c1a007239b46ef/properdocs-1.6.7.tar.gz", hash = "sha256:adc7b16e562890af0e098a7e5b02e3a81c20894a87d6a28d345c9300de73c26e", size = 276141, upload-time = "2026-03-20T20:07:48.167Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ec/29/f27a4e1eddf72ed3db6e47818fbafe6debbf09fd7051f9c1a007239b46ef/properdocs-1.6.7.tar.gz", hash = "sha256:adc7b16e562890af0e098a7e5b02e3a81c20894a87d6a28d345c9300de73c26e", size = 276141, upload-time = "2026-03-20T20:07:48.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/4d/fc923f5c85318ee8cc903566dc4e0ebe41b2dfc1d2ecf5546db232397ed6/properdocs-1.6.7-py3-none-any.whl", hash = "sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd", size = 225406, upload-time = "2026-03-20T20:07:46.875Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/4d/fc923f5c85318ee8cc903566dc4e0ebe41b2dfc1d2ecf5546db232397ed6/properdocs-1.6.7-py3-none-any.whl", hash = "sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd", size = 225406, upload-time = "2026-03-20T20:07:46.875Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] [[package]] name = "psutil" version = "7.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, - { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, - { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, - { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, - { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, - { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, - { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, - { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, - { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, - { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, - { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, - { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, - { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, - { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, - { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, - { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] name = "ptyprocess" version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] [[package]] name = "pure-eval" version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] [[package]] name = "py-cpuinfo" version = "9.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, ] [[package]] name = "py-partiql-parser" version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/7a/a0f6bda783eb4df8e3dfd55973a1ac6d368a89178c300e1b5b91cd181e5e/py_partiql_parser-0.6.3.tar.gz", hash = "sha256:09cecf916ce6e3da2c050f0cb6106166de42c33d34a078ec2eb19377ea70389a", size = 17456, upload-time = "2025-10-18T13:56:13.441Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/56/7a/a0f6bda783eb4df8e3dfd55973a1ac6d368a89178c300e1b5b91cd181e5e/py_partiql_parser-0.6.3.tar.gz", hash = "sha256:09cecf916ce6e3da2c050f0cb6106166de42c33d34a078ec2eb19377ea70389a", size = 17456, upload-time = "2025-10-18T13:56:13.441Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/33/a7cbfccc39056a5cf8126b7aab4c8bafbedd4f0ca68ae40ecb627a2d2cd3/py_partiql_parser-0.6.3-py2.py3-none-any.whl", hash = "sha256:deb0769c3346179d2f590dcbde556f708cdb929059fb654bad75f4cf6e07f582", size = 23752, upload-time = "2025-10-18T13:56:12.256Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c9/33/a7cbfccc39056a5cf8126b7aab4c8bafbedd4f0ca68ae40ecb627a2d2cd3/py_partiql_parser-0.6.3-py2.py3-none-any.whl", hash = "sha256:deb0769c3346179d2f590dcbde556f708cdb929059fb654bad75f4cf6e07f582", size = 23752, upload-time = "2025-10-18T13:56:12.256Z" }, ] [[package]] name = "py-serializable" version = "2.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "defusedxml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, ] [[package]] name = "py-spy" version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/93/d8/5b71371f50cf153b1307e5a11ac8a4ce4d85651dae946bd7e9a064146545/py_spy-0.4.2.tar.gz", hash = "sha256:90e600b27bb6bb40479637baca5a5b4bc2ba3395c93d889e672315d93042c4ae", size = 286374, upload-time = "2026-04-24T22:08:54.906Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/93/d8/5b71371f50cf153b1307e5a11ac8a4ce4d85651dae946bd7e9a064146545/py_spy-0.4.2.tar.gz", hash = "sha256:90e600b27bb6bb40479637baca5a5b4bc2ba3395c93d889e672315d93042c4ae", size = 286374, upload-time = "2026-04-24T22:08:54.906Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/21/ec030145a0c7992bd4b9eafb2f06f56358b3a5339eab4a16534baf3c69aa/py_spy-0.4.2-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1ccf688393105111684435f035bc14ec3f22117dd2b85b2414612cf27a22755a", size = 3743992, upload-time = "2026-04-24T22:08:45.438Z" }, - { url = "https://files.pythonhosted.org/packages/50/80/de5fd27243c2be03692ecd317bf0dbe24b4c6f78f689ce111e7277a7cb09/py_spy-0.4.2-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:a0e6f6810ccf0fc5e64e85e0182a5b626c4496eec01b14fb8755154b363a4831", size = 1859057, upload-time = "2026-04-24T22:08:46.946Z" }, - { url = "https://files.pythonhosted.org/packages/89/23/3eb4c23c684ebd667674ce1d076ae855e0621d1d9bd5e052aa3f7982f757/py_spy-0.4.2-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:142887e984a4e541071c99a4401ff8c3770f255d329dbd0f64e8c1dd51882cce", size = 2828136, upload-time = "2026-04-24T22:08:48.519Z" }, - { url = "https://files.pythonhosted.org/packages/ca/01/6314152cf9ad3310ebacbf2c47b5ed858086530f8e12b1a665725ca5e0f4/py_spy-0.4.2-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1c6d9b0e2379ead5bf792df43f4cf36153aa79e6dda4fb8ac7740cf8017110", size = 2857707, upload-time = "2026-04-24T22:08:49.677Z" }, - { url = "https://files.pythonhosted.org/packages/cc/1f/0960a129d504728d28a51dbd5a04ce94031eb75bac676341da7aefdd8232/py_spy-0.4.2-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24720573f95230653b457671a1dcc3c5a381fcf4e92677761e328a430ad251b2", size = 2301852, upload-time = "2026-04-24T22:08:51.152Z" }, - { url = "https://files.pythonhosted.org/packages/f9/34/dd7d3c763a00b7b965e25a5eab0acd1a345dbaf0f45fffe595278873a1c0/py_spy-0.4.2-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:aeb0323409199c785f730645e9f4bb7a7b9ca2c481f2c331a55642b5d13fa52f", size = 2936518, upload-time = "2026-04-24T22:08:52.264Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ed/1409cdb557e558a6c98003ab12fdd4284699e158c167c187cb0f124eea4c/py_spy-0.4.2-py2.py3-none-win_amd64.whl", hash = "sha256:8b06a353c177677e4e1701b288d8c58e2f8d4208ee81a8048d9f72ba800918f8", size = 1894002, upload-time = "2026-04-24T22:08:53.811Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ef/21/ec030145a0c7992bd4b9eafb2f06f56358b3a5339eab4a16534baf3c69aa/py_spy-0.4.2-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1ccf688393105111684435f035bc14ec3f22117dd2b85b2414612cf27a22755a", size = 3743992, upload-time = "2026-04-24T22:08:45.438Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/50/80/de5fd27243c2be03692ecd317bf0dbe24b4c6f78f689ce111e7277a7cb09/py_spy-0.4.2-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:a0e6f6810ccf0fc5e64e85e0182a5b626c4496eec01b14fb8755154b363a4831", size = 1859057, upload-time = "2026-04-24T22:08:46.946Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/89/23/3eb4c23c684ebd667674ce1d076ae855e0621d1d9bd5e052aa3f7982f757/py_spy-0.4.2-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:142887e984a4e541071c99a4401ff8c3770f255d329dbd0f64e8c1dd51882cce", size = 2828136, upload-time = "2026-04-24T22:08:48.519Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ca/01/6314152cf9ad3310ebacbf2c47b5ed858086530f8e12b1a665725ca5e0f4/py_spy-0.4.2-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1c6d9b0e2379ead5bf792df43f4cf36153aa79e6dda4fb8ac7740cf8017110", size = 2857707, upload-time = "2026-04-24T22:08:49.677Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cc/1f/0960a129d504728d28a51dbd5a04ce94031eb75bac676341da7aefdd8232/py_spy-0.4.2-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24720573f95230653b457671a1dcc3c5a381fcf4e92677761e328a430ad251b2", size = 2301852, upload-time = "2026-04-24T22:08:51.152Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f9/34/dd7d3c763a00b7b965e25a5eab0acd1a345dbaf0f45fffe595278873a1c0/py_spy-0.4.2-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:aeb0323409199c785f730645e9f4bb7a7b9ca2c481f2c331a55642b5d13fa52f", size = 2936518, upload-time = "2026-04-24T22:08:52.264Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6f/ed/1409cdb557e558a6c98003ab12fdd4284699e158c167c187cb0f124eea4c/py_spy-0.4.2-py2.py3-none-win_amd64.whl", hash = "sha256:8b06a353c177677e4e1701b288d8c58e2f8d4208ee81a8048d9f72ba800918f8", size = 1894002, upload-time = "2026-04-24T22:08:53.811Z" }, ] [[package]] name = "pycparser" version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } 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" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } 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" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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]] name = "pydantic-core" version = "2.46.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] [[package]] name = "pydantic-settings" version = "2.14.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] name = "pygments" version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pyjwt" version = "2.13.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, ] [[package]] name = "pymdown-extensions" version = "10.21.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, ] [[package]] name = "pyparsing" version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] name = "pyproject-hooks" version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, ] [[package]] name = "pyright" version = "1.1.410" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/53/e4d8ea1391bd4355231be6f91bf239479aa0014260ed3fb5526eeb12a1f2/pyright-1.1.410.tar.gz", hash = "sha256:07a073b8ba6749826773c1269773efa11b93440d9a6aa60419d9a3172d6dc488", size = 4062013, upload-time = "2026-06-01T17:35:48.894Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/10/53/e4d8ea1391bd4355231be6f91bf239479aa0014260ed3fb5526eeb12a1f2/pyright-1.1.410.tar.gz", hash = "sha256:07a073b8ba6749826773c1269773efa11b93440d9a6aa60419d9a3172d6dc488", size = 4062013, upload-time = "2026-06-01T17:35:48.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/33/288b5868fa00846dacf249633719d747893e54aebd196b9968ac1878a5d3/pyright-1.1.410-py3-none-any.whl", hash = "sha256:5e961bed37cacf96b3f7cd7b1da39b350a9239aa2e69138d0e88f728cfaf296c", size = 6082448, upload-time = "2026-06-01T17:35:46.387Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d7/33/288b5868fa00846dacf249633719d747893e54aebd196b9968ac1878a5d3/pyright-1.1.410-py3-none-any.whl", hash = "sha256:5e961bed37cacf96b3f7cd7b1da39b350a9239aa2e69138d0e88f728cfaf296c", size = 6082448, upload-time = "2026-06-01T17:35:46.387Z" }, ] [[package]] name = "pytest" version = "9.1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, @@ -3416,179 +3648,189 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "pytest-asyncio" version = "1.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] [[package]] name = "pytest-benchmark" version = "5.2.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "py-cpuinfo" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, ] [[package]] name = "pytest-cov" version = "7.1.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } 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" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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 = "pytest-env" -version = "1.1.5" -source = { registry = "https://pypi.org/simple" } +version = "1.6.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "pytest" }, + { name = "python-dotenv" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911, upload-time = "2024-09-17T22:39:18.566Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ff/69/4db1c30625af0621df8dbe73797b38b6d1b04e15d021dd5d26a6d297f78c/pytest_env-1.6.0.tar.gz", hash = "sha256:ac02d6fba16af54d61e311dd70a3c61024a4e966881ea844affc3c8f0bf207d3", size = 16163, upload-time = "2026-03-12T22:39:43.78Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141, upload-time = "2024-09-17T22:39:16.942Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/27/16/ad52f56b96d851a2bcfdc1e754c3531341885bd7177a128c13ff2ca72ab4/pytest_env-1.6.0-py3-none-any.whl", hash = "sha256:1e7f8a62215e5885835daaed694de8657c908505b964ec8097a7ce77b403d9a3", size = 10400, upload-time = "2026-03-12T22:39:41.887Z" }, ] [[package]] name = "pytest-html" version = "4.2.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "jinja2" }, { name = "pytest" }, { name = "pytest-metadata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/08/2076aa09507e51c1119d16a84c6307354d16270558f1a44fc9a2c99fdf1d/pytest_html-4.2.0.tar.gz", hash = "sha256:b6a88cba507500d8709959201e2e757d3941e859fd17cfd4ed87b16fc0c67912", size = 108634, upload-time = "2026-01-19T11:25:26.471Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c4/08/2076aa09507e51c1119d16a84c6307354d16270558f1a44fc9a2c99fdf1d/pytest_html-4.2.0.tar.gz", hash = "sha256:b6a88cba507500d8709959201e2e757d3941e859fd17cfd4ed87b16fc0c67912", size = 108634, upload-time = "2026-01-19T11:25:26.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/47/07046e0acedc12fe2bae79cf6c73ad67f51ae9d67df64d06b0f3eac73d36/pytest_html-4.2.0-py3-none-any.whl", hash = "sha256:ff5caf3e17a974008e5816edda61168e6c3da442b078a44f8744865862a85636", size = 23801, upload-time = "2026-01-19T11:25:25.008Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/84/47/07046e0acedc12fe2bae79cf6c73ad67f51ae9d67df64d06b0f3eac73d36/pytest_html-4.2.0-py3-none-any.whl", hash = "sha256:ff5caf3e17a974008e5816edda61168e6c3da442b078a44f8744865862a85636", size = 23801, upload-time = "2026-01-19T11:25:25.008Z" }, ] [[package]] name = "pytest-metadata" version = "3.1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952, upload-time = "2024-02-12T19:38:44.887Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952, upload-time = "2024-02-12T19:38:44.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428, upload-time = "2024-02-12T19:38:42.531Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428, upload-time = "2024-02-12T19:38:42.531Z" }, ] [[package]] name = "pytest-mock" version = "3.15.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] name = "pytest-timeout" version = "2.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] [[package]] name = "pytest-xdist" version = "3.8.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "execnet" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-discovery" version = "1.4.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, ] [[package]] name = "python-dotenv" version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "python-gitlab" version = "6.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "requests" }, { name = "requests-toolbelt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/bd/b30f1d3b303cb5d3c72e2d57a847d699e8573cbdfd67ece5f1795e49da1c/python_gitlab-6.5.0.tar.gz", hash = "sha256:97553652d94b02de343e9ca92782239aa2b5f6594c5482331a9490d9d5e8737d", size = 400591, upload-time = "2025-10-17T21:40:02.89Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9a/bd/b30f1d3b303cb5d3c72e2d57a847d699e8573cbdfd67ece5f1795e49da1c/python_gitlab-6.5.0.tar.gz", hash = "sha256:97553652d94b02de343e9ca92782239aa2b5f6594c5482331a9490d9d5e8737d", size = 400591, upload-time = "2025-10-17T21:40:02.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/bd/b0d440685fbcafee462bed793a74aea88541887c4c30556a55ac64914b8d/python_gitlab-6.5.0-py3-none-any.whl", hash = "sha256:494e1e8e5edd15286eaf7c286f3a06652688f1ee20a49e2a0218ddc5cc475e32", size = 144419, upload-time = "2025-10-17T21:40:01.233Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/34/bd/b0d440685fbcafee462bed793a74aea88541887c4c30556a55ac64914b8d/python_gitlab-6.5.0-py3-none-any.whl", hash = "sha256:494e1e8e5edd15286eaf7c286f3a06652688f1ee20a49e2a0218ddc5cc475e32", size = 144419, upload-time = "2025-10-17T21:40:01.233Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] [[package]] name = "python-semantic-release" version = "10.5.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "click" }, { name = "click-option-group" }, @@ -3604,769 +3846,775 @@ dependencies = [ { name = "shellingham" }, { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/3a/7332b822825ed0e902c6e950e0d1e90e8f666fd12eb27855d1c8b6677eff/python_semantic_release-10.5.3.tar.gz", hash = "sha256:de4da78635fa666e5774caaca2be32063cae72431eb75e2ac23b9f2dfd190785", size = 618034, upload-time = "2025-12-14T22:37:29.782Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/3a/7332b822825ed0e902c6e950e0d1e90e8f666fd12eb27855d1c8b6677eff/python_semantic_release-10.5.3.tar.gz", hash = "sha256:de4da78635fa666e5774caaca2be32063cae72431eb75e2ac23b9f2dfd190785", size = 618034, upload-time = "2025-12-14T22:37:29.782Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/01/ada29a1215df601bded0a2efd3b6d53864a0a9e0a9ea52aeaebe14fd03fd/python_semantic_release-10.5.3-py3-none-any.whl", hash = "sha256:1be0e07c36fa1f1ec9da4f438c1f6bbd7bc10eb0d6ac0089b0643103708c2823", size = 152716, upload-time = "2025-12-14T22:37:28.089Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6b/01/ada29a1215df601bded0a2efd3b6d53864a0a9e0a9ea52aeaebe14fd03fd/python_semantic_release-10.5.3-py3-none-any.whl", hash = "sha256:1be0e07c36fa1f1ec9da4f438c1f6bbd7bc10eb0d6ac0089b0643103708c2823", size = 152716, upload-time = "2025-12-14T22:37:28.089Z" }, ] [[package]] name = "pywin32" -version = "312" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, - { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, - { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, - { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, - { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, - { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, - { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, - { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, - { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, - { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, - { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, - { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, - { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, +version = "311" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] [[package]] name = "pywin32-ctypes" version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "pyyaml-env-tag" version = "1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] [[package]] name = "radon" version = "6.0.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "colorama" }, { name = "mando" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/6d/98e61600febf6bd929cf04154537c39dc577ce414bafbfc24a286c4fa76d/radon-6.0.1.tar.gz", hash = "sha256:d1ac0053943a893878940fedc8b19ace70386fc9c9bf0a09229a44125ebf45b5", size = 1874992, upload-time = "2023-03-26T06:24:38.868Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b1/6d/98e61600febf6bd929cf04154537c39dc577ce414bafbfc24a286c4fa76d/radon-6.0.1.tar.gz", hash = "sha256:d1ac0053943a893878940fedc8b19ace70386fc9c9bf0a09229a44125ebf45b5", size = 1874992, upload-time = "2023-03-26T06:24:38.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl", hash = "sha256:632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859", size = 52784, upload-time = "2023-03-26T06:24:33.949Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl", hash = "sha256:632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859", size = 52784, upload-time = "2023-03-26T06:24:33.949Z" }, ] [[package]] name = "readme-renderer" version = "45.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "docutils" }, { name = "nh3" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", size = 36172, upload-time = "2026-06-09T21:05:17.37Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", size = 36172, upload-time = "2026-06-09T21:05:17.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f", size = 14134, upload-time = "2026-06-09T21:05:15.85Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f", size = 14134, upload-time = "2026-06-09T21:05:15.85Z" }, ] [[package]] name = "referencing" version = "0.37.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "attrs" }, - { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" }, marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "regex" version = "2026.5.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44", size = 489438, upload-time = "2026-05-09T23:11:29.374Z" }, - { url = "https://files.pythonhosted.org/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a", size = 291270, upload-time = "2026-05-09T23:11:33.254Z" }, - { url = "https://files.pythonhosted.org/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733", size = 289198, upload-time = "2026-05-09T23:11:35.769Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2", size = 784765, upload-time = "2026-05-09T23:11:37.689Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea", size = 852115, upload-time = "2026-05-09T23:11:39.973Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538", size = 899503, upload-time = "2026-05-09T23:11:42.48Z" }, - { url = "https://files.pythonhosted.org/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2", size = 794093, upload-time = "2026-05-09T23:11:44.681Z" }, - { url = "https://files.pythonhosted.org/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989", size = 786234, upload-time = "2026-05-09T23:11:46.882Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9", size = 769895, upload-time = "2026-05-09T23:11:49.324Z" }, - { url = "https://files.pythonhosted.org/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00", size = 774991, upload-time = "2026-05-09T23:11:51.261Z" }, - { url = "https://files.pythonhosted.org/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808", size = 848790, upload-time = "2026-05-09T23:11:53.232Z" }, - { url = "https://files.pythonhosted.org/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248", size = 757679, upload-time = "2026-05-09T23:11:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6", size = 837116, upload-time = "2026-05-09T23:11:57.934Z" }, - { url = "https://files.pythonhosted.org/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4", size = 782081, upload-time = "2026-05-09T23:11:59.607Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac", size = 266247, upload-time = "2026-05-09T23:12:01.116Z" }, - { url = "https://files.pythonhosted.org/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03", size = 278416, upload-time = "2026-05-09T23:12:03.2Z" }, - { url = "https://files.pythonhosted.org/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b", size = 270413, upload-time = "2026-05-09T23:12:04.649Z" }, - { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, - { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, - { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, - { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, - { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, - { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, - { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, - { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, - { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, - { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, - { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, - { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, - { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, - { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, - { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, - { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, - { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, - { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, - { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, - { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, - { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, - { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, - { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, - { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, - { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, - { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, - { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, - { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, - { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, - { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, - { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, - { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, - { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, - { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, - { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, - { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, - { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, - { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, - { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, - { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, - { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, - { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, - { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, - { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, - { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, - { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, - { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, - { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, - { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, - { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, - { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44", size = 489438, upload-time = "2026-05-09T23:11:29.374Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a", size = 291270, upload-time = "2026-05-09T23:11:33.254Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733", size = 289198, upload-time = "2026-05-09T23:11:35.769Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2", size = 784765, upload-time = "2026-05-09T23:11:37.689Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea", size = 852115, upload-time = "2026-05-09T23:11:39.973Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538", size = 899503, upload-time = "2026-05-09T23:11:42.48Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2", size = 794093, upload-time = "2026-05-09T23:11:44.681Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989", size = 786234, upload-time = "2026-05-09T23:11:46.882Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9", size = 769895, upload-time = "2026-05-09T23:11:49.324Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00", size = 774991, upload-time = "2026-05-09T23:11:51.261Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808", size = 848790, upload-time = "2026-05-09T23:11:53.232Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248", size = 757679, upload-time = "2026-05-09T23:11:55.82Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6", size = 837116, upload-time = "2026-05-09T23:11:57.934Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4", size = 782081, upload-time = "2026-05-09T23:11:59.607Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac", size = 266247, upload-time = "2026-05-09T23:12:01.116Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03", size = 278416, upload-time = "2026-05-09T23:12:03.2Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b", size = 270413, upload-time = "2026-05-09T23:12:04.649Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, ] [[package]] name = "requests" version = "2.34.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/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" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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 = "requests-mock" version = "1.12.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, ] [[package]] name = "requests-toolbelt" version = "1.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] [[package]] name = "responses" version = "0.26.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "pyyaml" }, { name = "requests" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/58/1fb6de3503428196df78638f991ec8095274f1ee9723e272ee4d9ff0092b/responses-0.26.1.tar.gz", hash = "sha256:2eb3218553cc8f79b57d257bac23af5e1bf381f5b9390b1767816f0843e01dc2", size = 83088, upload-time = "2026-05-21T19:56:39.747Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/58/1fb6de3503428196df78638f991ec8095274f1ee9723e272ee4d9ff0092b/responses-0.26.1.tar.gz", hash = "sha256:2eb3218553cc8f79b57d257bac23af5e1bf381f5b9390b1767816f0843e01dc2", size = 83088, upload-time = "2026-05-21T19:56:39.747Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/31/6a620b4427d546b9e7cca8b3b8c5f0559d9cef2bb9eedcda7f73c1473c19/responses-0.26.1-py3-none-any.whl", hash = "sha256:8aacc4586eb08fb2208ef64a9eb4258d9b0c6e6f4260845f2f018ab847495345", size = 35502, upload-time = "2026-05-21T19:56:38.046Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3a/31/6a620b4427d546b9e7cca8b3b8c5f0559d9cef2bb9eedcda7f73c1473c19/responses-0.26.1-py3-none-any.whl", hash = "sha256:8aacc4586eb08fb2208ef64a9eb4258d9b0c6e6f4260845f2f018ab847495345", size = 35502, upload-time = "2026-05-21T19:56:38.046Z" }, ] [[package]] name = "rfc3339-validator" version = "0.1.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, ] [[package]] name = "rfc3986" version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, ] [[package]] -name = "rfc3987" -version = "1.3.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/bb/f1395c4b62f251a1cb503ff884500ebd248eed593f41b469f89caa3547bd/rfc3987-1.3.8.tar.gz", hash = "sha256:d3c4d257a560d544e9826b38bc81db676890c79ab9d7ac92b39c7a253d5ca733", size = 20700, upload-time = "2018-07-29T17:23:47.954Z" } +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/d4/f7407c3d15d5ac779c3dd34fbbc6ea2090f77bd7dd12f207ccf881551208/rfc3987-1.3.8-py2.py3-none-any.whl", hash = "sha256:10702b1e51e5658843460b189b185c0366d2cf4cff716f13111b0ea9fd2dce53", size = 13377, upload-time = "2018-07-29T17:23:45.313Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, ] [[package]] name = "rich" version = "14.3.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, ] [[package]] name = "rich-argparse" version = "1.8.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/e5/1064c43203a357d668cd42435f7a15fe6af51512d85b2104fecb937aa861/rich_argparse-1.8.0.tar.gz", hash = "sha256:679df3d832fa94ad6e4bdb07ded088cd7ea2dddc58ae9b2b46346a40b06cbc0c", size = 38940, upload-time = "2026-05-01T15:18:43.604Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/e5/1064c43203a357d668cd42435f7a15fe6af51512d85b2104fecb937aa861/rich_argparse-1.8.0.tar.gz", hash = "sha256:679df3d832fa94ad6e4bdb07ded088cd7ea2dddc58ae9b2b46346a40b06cbc0c", size = 38940, upload-time = "2026-05-01T15:18:43.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl", hash = "sha256:d2a3ce7854654e2253c578763ab0a32f05016f23a55fadba7b9a91b6c0e92142", size = 25616, upload-time = "2026-05-01T15:18:42.395Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0b/35/1cceccc5fcb50fa2ed53e2aa278cd032f3902682a73e763fb1ac3be8e6fa/rich_argparse-1.8.0-py3-none-any.whl", hash = "sha256:d2a3ce7854654e2253c578763ab0a32f05016f23a55fadba7b9a91b6c0e92142", size = 25616, upload-time = "2026-05-01T15:18:42.395Z" }, ] [[package]] name = "rpds-py" version = "0.30.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } resolution-markers = [ "python_full_version < '3.11'", ] -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, - { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, - { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, - { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, - { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, - { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, - { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, - { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, - { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, - { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, - { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, - { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, - { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, - { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, - { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] [[package]] name = "rpds-py" version = "2026.5.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/a0/acf8b6fc20bfdcd3a45bd3f57680fb198e157b7e997b9123b10763798bd2/rpds_py-2026.5.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036", size = 355609, upload-time = "2026-05-28T11:58:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/b6/95/f8203fd997484b1690a6869cd0e503b6c3c6be55b0ecc36d1a491fe742f0/rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc", size = 348460, upload-time = "2026-05-28T11:58:52.374Z" }, - { url = "https://files.pythonhosted.org/packages/33/8c/b47326ad2f0be545a5e5c1a55937a12afaea7d392ba2837bb9680f57e6c9/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164", size = 381031, upload-time = "2026-05-28T11:58:53.775Z" }, - { url = "https://files.pythonhosted.org/packages/22/0b/e83bbd97ffac6f6389b605cd4e1c8ac5761dc7e977769c9255d8c5adb7bd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead", size = 387121, upload-time = "2026-05-28T11:58:55.243Z" }, - { url = "https://files.pythonhosted.org/packages/fd/0e/d285d1bc8864245919c61e1ca82263e4a66d337759c3a4cef72766ff9afc/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece", size = 501026, upload-time = "2026-05-28T11:58:56.788Z" }, - { url = "https://files.pythonhosted.org/packages/86/06/ccb2109a1e543437b5e43816f2b43b9554cc6783145528a4e3711e05c011/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb", size = 391865, upload-time = "2026-05-28T11:58:58.298Z" }, - { url = "https://files.pythonhosted.org/packages/3d/33/237173db1cfef10105b3839a24de00eb8d2a523711add4632447cdf0aedd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda", size = 378012, upload-time = "2026-05-28T11:58:59.589Z" }, - { url = "https://files.pythonhosted.org/packages/97/64/1eae54e34d5161f9969295e80bd6b62a55f2b6ac5f2a5b60d02c2140e758/rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a", size = 391111, upload-time = "2026-05-28T11:59:01.104Z" }, - { url = "https://files.pythonhosted.org/packages/d8/34/5bb334a5a0f65d77869217c4654f34c78a7d11b93938a3c076a2edeafc52/rpds_py-2026.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0", size = 409225, upload-time = "2026-05-28T11:59:02.433Z" }, - { url = "https://files.pythonhosted.org/packages/16/0f/007ec21283b5b040b4ec3bd95e0402591e22bfa7d5c93dfe01c465c2d2d7/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a", size = 556487, upload-time = "2026-05-28T11:59:04.012Z" }, - { url = "https://files.pythonhosted.org/packages/ff/10/5437c94508169b6b22d8418fef7a66e9ffb5f3b9e9c94460f2eedafe06ff/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2", size = 620798, upload-time = "2026-05-28T11:59:05.485Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d5/9937dce4d6bda74157b954e7d1460db05a22f5929dccfeeba1ed27a93df0/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2", size = 584053, upload-time = "2026-05-28T11:59:06.837Z" }, - { url = "https://files.pythonhosted.org/packages/6c/31/750617dd0ae1752471bf43f9e41d263398fae7cde7849d23b8574a70e617/rpds_py-2026.5.1-cp311-cp311-win32.whl", hash = "sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f", size = 214390, upload-time = "2026-05-28T11:59:08.402Z" }, - { url = "https://files.pythonhosted.org/packages/3c/bb/3dcab0e1d9516303f2eb672a5d6f62eca5a69e2886301e9c8c54b520c39b/rpds_py-2026.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a", size = 231097, upload-time = "2026-05-28T11:59:09.786Z" }, - { url = "https://files.pythonhosted.org/packages/49/d6/c6bbf5cb1cf12b9732df8074b57f6ef8341ba884c95d40632ae8bddb44e4/rpds_py-2026.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b", size = 226361, upload-time = "2026-05-28T11:59:11.079Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" }, - { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" }, - { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" }, - { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" }, - { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" }, - { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" }, - { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" }, - { url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" }, - { url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" }, - { url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" }, - { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, - { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, - { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, - { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, - { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, - { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, - { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, - { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, - { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, - { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, - { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, - { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, - { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, - { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, - { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, - { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, - { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, - { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, - { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, - { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, - { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, - { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, - { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, - { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, - { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, - { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, - { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, - { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, - { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, - { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, - { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, - { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, - { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, - { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, - { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, - { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, - { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, - { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, - { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, - { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, - { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, - { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, - { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, - { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, - { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, - { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, - { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, - { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, - { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, - { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, - { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, - { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, - { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, - { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, - { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, - { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, - { url = "https://files.pythonhosted.org/packages/42/56/3fe0fb34820ff667be791b3a3c22b85e8bcba54e9c832f47438c191fa7be/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea", size = 357151, upload-time = "2026-05-28T12:01:53.43Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/3eb9ccdb9f143b8c9b003978898cb497f942a324c077401e6b8834238e63/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb", size = 350195, upload-time = "2026-05-28T12:01:54.901Z" }, - { url = "https://files.pythonhosted.org/packages/a7/24/dbda232bc4f3ed732120692ab0d2c8402cb020516556d8bee622dcef2413/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df", size = 381850, upload-time = "2026-05-28T12:01:56.601Z" }, - { url = "https://files.pythonhosted.org/packages/40/30/32e769839a358f78810c234f160f2cc21d1e4e47e1c0e0e0d535be5a0219/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7", size = 387899, upload-time = "2026-05-28T12:01:58.212Z" }, - { url = "https://files.pythonhosted.org/packages/ab/86/ec84d243aadb3b34b71dd26a010d0930b2d284ff5fc9a69fec53810ee6fd/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc", size = 501618, upload-time = "2026-05-28T12:01:59.888Z" }, - { url = "https://files.pythonhosted.org/packages/74/25/b60e52686bbff777a64f9e4f4d3dd57980dc846913777177a2c92e4937aa/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162", size = 394003, upload-time = "2026-05-28T12:02:01.482Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c7/b3a6a588cc2219510ef3f42e207483a93950bedd1e3a0fd4015c95cff9e5/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251", size = 379778, upload-time = "2026-05-28T12:02:03.197Z" }, - { url = "https://files.pythonhosted.org/packages/31/00/c7dba3fc8a3da8cb3f6db1eb3386be4d79c2e97c6890d20eb9ac66ae8c43/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a", size = 392359, upload-time = "2026-05-28T12:02:04.817Z" }, - { url = "https://files.pythonhosted.org/packages/93/dd/472ba494c70753f93745992c99855bee0636daf74e6984e5e003f150316f/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b", size = 412820, upload-time = "2026-05-28T12:02:06.401Z" }, - { url = "https://files.pythonhosted.org/packages/1d/6f/93831a3bfe789542ed0c1d0d74b78b440f055d6dc3ea4640eba2d95e6e23/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34", size = 557243, upload-time = "2026-05-28T12:02:08.013Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ff/0b3d604614ffc77522c6b288fdbce68957eb583da1002aa65ba38ac0ee40/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c", size = 623541, upload-time = "2026-05-28T12:02:09.661Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ea/e7b0251441da9adfeaebcf29601d10f2a1455fcf0772fae9e7e19032bd96/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049", size = 586326, upload-time = "2026-05-28T12:02:11.47Z" }, +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4f/a0/acf8b6fc20bfdcd3a45bd3f57680fb198e157b7e997b9123b10763798bd2/rpds_py-2026.5.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036", size = 355609, upload-time = "2026-05-28T11:58:50.78Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b6/95/f8203fd997484b1690a6869cd0e503b6c3c6be55b0ecc36d1a491fe742f0/rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc", size = 348460, upload-time = "2026-05-28T11:58:52.374Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/33/8c/b47326ad2f0be545a5e5c1a55937a12afaea7d392ba2837bb9680f57e6c9/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164", size = 381031, upload-time = "2026-05-28T11:58:53.775Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/0b/e83bbd97ffac6f6389b605cd4e1c8ac5761dc7e977769c9255d8c5adb7bd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead", size = 387121, upload-time = "2026-05-28T11:58:55.243Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fd/0e/d285d1bc8864245919c61e1ca82263e4a66d337759c3a4cef72766ff9afc/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece", size = 501026, upload-time = "2026-05-28T11:58:56.788Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/86/06/ccb2109a1e543437b5e43816f2b43b9554cc6783145528a4e3711e05c011/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb", size = 391865, upload-time = "2026-05-28T11:58:58.298Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3d/33/237173db1cfef10105b3839a24de00eb8d2a523711add4632447cdf0aedd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda", size = 378012, upload-time = "2026-05-28T11:58:59.589Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/97/64/1eae54e34d5161f9969295e80bd6b62a55f2b6ac5f2a5b60d02c2140e758/rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a", size = 391111, upload-time = "2026-05-28T11:59:01.104Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d8/34/5bb334a5a0f65d77869217c4654f34c78a7d11b93938a3c076a2edeafc52/rpds_py-2026.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0", size = 409225, upload-time = "2026-05-28T11:59:02.433Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/16/0f/007ec21283b5b040b4ec3bd95e0402591e22bfa7d5c93dfe01c465c2d2d7/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a", size = 556487, upload-time = "2026-05-28T11:59:04.012Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ff/10/5437c94508169b6b22d8418fef7a66e9ffb5f3b9e9c94460f2eedafe06ff/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2", size = 620798, upload-time = "2026-05-28T11:59:05.485Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/d5/9937dce4d6bda74157b954e7d1460db05a22f5929dccfeeba1ed27a93df0/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2", size = 584053, upload-time = "2026-05-28T11:59:06.837Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6c/31/750617dd0ae1752471bf43f9e41d263398fae7cde7849d23b8574a70e617/rpds_py-2026.5.1-cp311-cp311-win32.whl", hash = "sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f", size = 214390, upload-time = "2026-05-28T11:59:08.402Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/bb/3dcab0e1d9516303f2eb672a5d6f62eca5a69e2886301e9c8c54b520c39b/rpds_py-2026.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a", size = 231097, upload-time = "2026-05-28T11:59:09.786Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/49/d6/c6bbf5cb1cf12b9732df8074b57f6ef8341ba884c95d40632ae8bddb44e4/rpds_py-2026.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b", size = 226361, upload-time = "2026-05-28T11:59:11.079Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/56/3fe0fb34820ff667be791b3a3c22b85e8bcba54e9c832f47438c191fa7be/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea", size = 357151, upload-time = "2026-05-28T12:01:53.43Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8b/f2/3eb9ccdb9f143b8c9b003978898cb497f942a324c077401e6b8834238e63/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb", size = 350195, upload-time = "2026-05-28T12:01:54.901Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a7/24/dbda232bc4f3ed732120692ab0d2c8402cb020516556d8bee622dcef2413/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df", size = 381850, upload-time = "2026-05-28T12:01:56.601Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/40/30/32e769839a358f78810c234f160f2cc21d1e4e47e1c0e0e0d535be5a0219/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7", size = 387899, upload-time = "2026-05-28T12:01:58.212Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ab/86/ec84d243aadb3b34b71dd26a010d0930b2d284ff5fc9a69fec53810ee6fd/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc", size = 501618, upload-time = "2026-05-28T12:01:59.888Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/74/25/b60e52686bbff777a64f9e4f4d3dd57980dc846913777177a2c92e4937aa/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162", size = 394003, upload-time = "2026-05-28T12:02:01.482Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9b/c7/b3a6a588cc2219510ef3f42e207483a93950bedd1e3a0fd4015c95cff9e5/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251", size = 379778, upload-time = "2026-05-28T12:02:03.197Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/31/00/c7dba3fc8a3da8cb3f6db1eb3386be4d79c2e97c6890d20eb9ac66ae8c43/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a", size = 392359, upload-time = "2026-05-28T12:02:04.817Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/93/dd/472ba494c70753f93745992c99855bee0636daf74e6984e5e003f150316f/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b", size = 412820, upload-time = "2026-05-28T12:02:06.401Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1d/6f/93831a3bfe789542ed0c1d0d74b78b440f055d6dc3ea4640eba2d95e6e23/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34", size = 557243, upload-time = "2026-05-28T12:02:08.013Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1f/ff/0b3d604614ffc77522c6b288fdbce68957eb583da1002aa65ba38ac0ee40/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c", size = 623541, upload-time = "2026-05-28T12:02:09.661Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ea/ea/e7b0251441da9adfeaebcf29601d10f2a1455fcf0772fae9e7e19032bd96/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049", size = 586326, upload-time = "2026-05-28T12:02:11.47Z" }, ] [[package]] name = "ruamel-yaml" -version = "0.17.40" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d1/d6/eb2833ccba5ea36f8f4de4bcfa0d1a91eb618f832d430b70e3086821f251/ruamel.yaml-0.17.40.tar.gz", hash = "sha256:6024b986f06765d482b5b07e086cc4b4cd05dd22ddcbc758fa23d54873cf313d", size = 137672, upload-time = "2023-10-20T12:53:56.073Z" } +version = "0.19.1" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/79/5e2cffa1c77432f11cd93a5351f30732c997a239d3a3090856a72d6d8ba7/ruamel.yaml-0.17.40-py3-none-any.whl", hash = "sha256:b16b6c3816dff0a93dca12acf5e70afd089fa5acb80604afd1ffa8b465b7722c", size = 113666, upload-time = "2023-10-20T12:53:52.628Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, ] [[package]] name = "ruamel-yaml-clib" version = "0.2.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/5a/4ab767cd42dcd65b83c323e1620d7c01ee60a52f4032fb7b61501f45f5c2/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03", size = 147454, upload-time = "2025-11-16T16:13:02.54Z" }, - { url = "https://files.pythonhosted.org/packages/40/44/184173ac1e74fd35d308108bcbf83904d6ef8439c70763189225a166b238/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77", size = 132467, upload-time = "2025-11-16T16:13:03.539Z" }, - { url = "https://files.pythonhosted.org/packages/49/1b/2d2077a25fe682ae335007ca831aff42e3cbc93c14066675cf87a6c7fc3e/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4be366220090d7c3424ac2b71c90d1044ea34fca8c0b88f250064fd06087e614", size = 693454, upload-time = "2025-11-16T20:22:41.083Z" }, - { url = "https://files.pythonhosted.org/packages/90/16/e708059c4c429ad2e33be65507fc1730641e5f239fb2964efc1ba6edea94/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f66f600833af58bea694d5892453f2270695b92200280ee8c625ec5a477eed3", size = 700345, upload-time = "2025-11-16T16:13:04.771Z" }, - { url = "https://files.pythonhosted.org/packages/d9/79/0e8ef51df1f0950300541222e3332f20707a9c210b98f981422937d1278c/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da3d6adadcf55a93c214d23941aef4abfd45652110aed6580e814152f385b862", size = 731306, upload-time = "2025-11-16T16:13:06.312Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f4/2cdb54b142987ddfbd01fc45ac6bd882695fbcedb9d8bbf796adc3fc3746/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9fde97ecb7bb9c41261c2ce0da10323e9227555c674989f8d9eb7572fc2098d", size = 692415, upload-time = "2025-11-16T16:13:07.465Z" }, - { url = "https://files.pythonhosted.org/packages/a0/07/40b5fc701cce8240a3e2d26488985d3bbdc446e9fe397c135528d412fea6/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:05c70f7f86be6f7bee53794d80050a28ae7e13e4a0087c1839dcdefd68eb36b6", size = 705007, upload-time = "2025-11-16T20:22:42.856Z" }, - { url = "https://files.pythonhosted.org/packages/82/19/309258a1df6192fb4a77ffa8eae3e8150e8d0ffa56c1b6fa92e450ba2740/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f1d38cbe622039d111b69e9ca945e7e3efebb30ba998867908773183357f3ed", size = 723974, upload-time = "2025-11-16T16:13:08.72Z" }, - { url = "https://files.pythonhosted.org/packages/67/3a/d6ee8263b521bfceb5cd2faeb904a15936480f2bb01c7ff74a14ec058ca4/ruamel_yaml_clib-0.2.15-cp310-cp310-win32.whl", hash = "sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f", size = 102836, upload-time = "2025-11-16T16:13:10.27Z" }, - { url = "https://files.pythonhosted.org/packages/ed/03/92aeb5c69018387abc49a8bb4f83b54a0471d9ef48e403b24bac68f01381/ruamel_yaml_clib-0.2.15-cp310-cp310-win_amd64.whl", hash = "sha256:468858e5cbde0198337e6a2a78eda8c3fb148bdf4c6498eaf4bc9ba3f8e780bd", size = 121917, upload-time = "2025-11-16T16:13:12.145Z" }, - { url = "https://files.pythonhosted.org/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, - { url = "https://files.pythonhosted.org/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, - { url = "https://files.pythonhosted.org/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, - { url = "https://files.pythonhosted.org/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, - { url = "https://files.pythonhosted.org/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, - { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, - { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, - { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, - { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, - { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, - { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, - { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, - { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, - { url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, - { url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, - { url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, - { url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, - { url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, - { url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, - { url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, - { url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, - { url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, - { url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, - { url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, - { url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, - { url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f7/5a/4ab767cd42dcd65b83c323e1620d7c01ee60a52f4032fb7b61501f45f5c2/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03", size = 147454, upload-time = "2025-11-16T16:13:02.54Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/40/44/184173ac1e74fd35d308108bcbf83904d6ef8439c70763189225a166b238/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77", size = 132467, upload-time = "2025-11-16T16:13:03.539Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/49/1b/2d2077a25fe682ae335007ca831aff42e3cbc93c14066675cf87a6c7fc3e/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4be366220090d7c3424ac2b71c90d1044ea34fca8c0b88f250064fd06087e614", size = 693454, upload-time = "2025-11-16T20:22:41.083Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/90/16/e708059c4c429ad2e33be65507fc1730641e5f239fb2964efc1ba6edea94/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f66f600833af58bea694d5892453f2270695b92200280ee8c625ec5a477eed3", size = 700345, upload-time = "2025-11-16T16:13:04.771Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d9/79/0e8ef51df1f0950300541222e3332f20707a9c210b98f981422937d1278c/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da3d6adadcf55a93c214d23941aef4abfd45652110aed6580e814152f385b862", size = 731306, upload-time = "2025-11-16T16:13:06.312Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a6/f4/2cdb54b142987ddfbd01fc45ac6bd882695fbcedb9d8bbf796adc3fc3746/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9fde97ecb7bb9c41261c2ce0da10323e9227555c674989f8d9eb7572fc2098d", size = 692415, upload-time = "2025-11-16T16:13:07.465Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a0/07/40b5fc701cce8240a3e2d26488985d3bbdc446e9fe397c135528d412fea6/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:05c70f7f86be6f7bee53794d80050a28ae7e13e4a0087c1839dcdefd68eb36b6", size = 705007, upload-time = "2025-11-16T20:22:42.856Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/82/19/309258a1df6192fb4a77ffa8eae3e8150e8d0ffa56c1b6fa92e450ba2740/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f1d38cbe622039d111b69e9ca945e7e3efebb30ba998867908773183357f3ed", size = 723974, upload-time = "2025-11-16T16:13:08.72Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/67/3a/d6ee8263b521bfceb5cd2faeb904a15936480f2bb01c7ff74a14ec058ca4/ruamel_yaml_clib-0.2.15-cp310-cp310-win32.whl", hash = "sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f", size = 102836, upload-time = "2025-11-16T16:13:10.27Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/03/92aeb5c69018387abc49a8bb4f83b54a0471d9ef48e403b24bac68f01381/ruamel_yaml_clib-0.2.15-cp310-cp310-win_amd64.whl", hash = "sha256:468858e5cbde0198337e6a2a78eda8c3fb148bdf4c6498eaf4bc9ba3f8e780bd", size = 121917, upload-time = "2025-11-16T16:13:12.145Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, ] [[package]] name = "ruff" version = "0.15.18" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/98/1295ad5a5aa9bc85bdcdfa5d82fe7b49c61af5657df4f227637ff9de0da6/ruff-0.15.18.tar.gz", hash = "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", size = 4761437, upload-time = "2026-06-18T18:25:39.224Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/d0/686e984941269621e2be72612d5c1e461f8f7b38415a2a7d7a81c8ae6715/ruff-0.15.18-py3-none-linux_armv6l.whl", hash = "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", size = 10887308, upload-time = "2026-06-18T18:25:03.062Z" }, - { url = "https://files.pythonhosted.org/packages/ed/21/bc4123e3f5515ee99f8ce1eb93a14a0628fe4d1678663cd08f933ac16931/ruff-0.15.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", size = 11281305, upload-time = "2026-06-18T18:25:30.026Z" }, - { url = "https://files.pythonhosted.org/packages/51/93/4769464c25cf7ab2acb3c7dda9cad3d867eb41c59565b3e2a9d17249c90c/ruff-0.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", size = 10641215, upload-time = "2026-06-18T18:25:15.802Z" }, - { url = "https://files.pythonhosted.org/packages/6c/42/56926d17120db2c208d76bf60a1a019644dd9e91dc27f0f95c9caddb1366/ruff-0.15.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", size = 10957224, upload-time = "2026-06-18T18:25:36.955Z" }, - { url = "https://files.pythonhosted.org/packages/22/4f/d43fab8d8189afde803103022d000a8ef9f230616d436d52a8b2b8d63b50/ruff-0.15.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", size = 10699024, upload-time = "2026-06-18T18:25:05.707Z" }, - { url = "https://files.pythonhosted.org/packages/63/42/1e3e4c68bd408b9768cf3e439acbe2c78245225faef253f7028a0cdb63e0/ruff-0.15.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", size = 11491458, upload-time = "2026-06-18T18:25:20.275Z" }, - { url = "https://files.pythonhosted.org/packages/20/77/47a3484bea8521e14a203d98c389c5c97846675e4f02734672da4a69b52a/ruff-0.15.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", size = 12383752, upload-time = "2026-06-18T18:25:22.535Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ca/054159590787023d83b658a1a1819c4c8910114e7015069340b71c0961cb/ruff-0.15.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", size = 11577923, upload-time = "2026-06-18T18:25:10.702Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/d353d6b7bbd73cc0ec37f4463d7540e45e894338abdd9964eee0de332708/ruff-0.15.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", size = 11583925, upload-time = "2026-06-18T18:25:32.391Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4a/891f89b9c296ed3e5f3ece1a5629badc989d9a8fdaa30431aaf4774bc1c2/ruff-0.15.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", size = 11582834, upload-time = "2026-06-18T18:25:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/32/a3/ed9e370154bf85de360b93c03026157f02d4943b2d01ff4945f4429f8e8a/ruff-0.15.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", size = 10927328, upload-time = "2026-06-18T18:25:34.676Z" }, - { url = "https://files.pythonhosted.org/packages/f5/d1/5cf5909329fedb5d39d555ee818ba5cf4638e1a301b89785d34f2905bfcb/ruff-0.15.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", size = 10693187, upload-time = "2026-06-18T18:25:08.245Z" }, - { url = "https://files.pythonhosted.org/packages/fd/44/ff6c635cf2c4f4e7b618b6640da057376baa36014695487d88aed4794268/ruff-0.15.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", size = 11208721, upload-time = "2026-06-18T18:25:41.327Z" }, - { url = "https://files.pythonhosted.org/packages/88/d9/5baa2a30861adfb7022cf33c1e35b2fc18085b08c16f83eff4c7b99a5f48/ruff-0.15.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", size = 11678599, upload-time = "2026-06-18T18:25:13.607Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1a/0725a7cfdc32ff769efb96ee782bec882e16448c5d9e3be947ec4c04ce27/ruff-0.15.18-py3-none-win32.whl", hash = "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", size = 10901903, upload-time = "2026-06-18T18:25:24.755Z" }, - { url = "https://files.pythonhosted.org/packages/f3/51/805d9f6fb7970505c3504794a5ec350f605361b807fef4dcf214ebd35e72/ruff-0.15.18-py3-none-win_amd64.whl", hash = "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3", size = 12041189, upload-time = "2026-06-18T18:25:17.915Z" }, - { url = "https://files.pythonhosted.org/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/74/98/1295ad5a5aa9bc85bdcdfa5d82fe7b49c61af5657df4f227637ff9de0da6/ruff-0.15.18.tar.gz", hash = "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", size = 4761437, upload-time = "2026-06-18T18:25:39.224Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/d0/686e984941269621e2be72612d5c1e461f8f7b38415a2a7d7a81c8ae6715/ruff-0.15.18-py3-none-linux_armv6l.whl", hash = "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", size = 10887308, upload-time = "2026-06-18T18:25:03.062Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ed/21/bc4123e3f5515ee99f8ce1eb93a14a0628fe4d1678663cd08f933ac16931/ruff-0.15.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", size = 11281305, upload-time = "2026-06-18T18:25:30.026Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/51/93/4769464c25cf7ab2acb3c7dda9cad3d867eb41c59565b3e2a9d17249c90c/ruff-0.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", size = 10641215, upload-time = "2026-06-18T18:25:15.802Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6c/42/56926d17120db2c208d76bf60a1a019644dd9e91dc27f0f95c9caddb1366/ruff-0.15.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", size = 10957224, upload-time = "2026-06-18T18:25:36.955Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/4f/d43fab8d8189afde803103022d000a8ef9f230616d436d52a8b2b8d63b50/ruff-0.15.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", size = 10699024, upload-time = "2026-06-18T18:25:05.707Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/42/1e3e4c68bd408b9768cf3e439acbe2c78245225faef253f7028a0cdb63e0/ruff-0.15.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", size = 11491458, upload-time = "2026-06-18T18:25:20.275Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/20/77/47a3484bea8521e14a203d98c389c5c97846675e4f02734672da4a69b52a/ruff-0.15.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", size = 12383752, upload-time = "2026-06-18T18:25:22.535Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0a/ca/054159590787023d83b658a1a1819c4c8910114e7015069340b71c0961cb/ruff-0.15.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", size = 11577923, upload-time = "2026-06-18T18:25:10.702Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6d/ff/d353d6b7bbd73cc0ec37f4463d7540e45e894338abdd9964eee0de332708/ruff-0.15.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", size = 11583925, upload-time = "2026-06-18T18:25:32.391Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/4a/891f89b9c296ed3e5f3ece1a5629badc989d9a8fdaa30431aaf4774bc1c2/ruff-0.15.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", size = 11582834, upload-time = "2026-06-18T18:25:27.309Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/32/a3/ed9e370154bf85de360b93c03026157f02d4943b2d01ff4945f4429f8e8a/ruff-0.15.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", size = 10927328, upload-time = "2026-06-18T18:25:34.676Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f5/d1/5cf5909329fedb5d39d555ee818ba5cf4638e1a301b89785d34f2905bfcb/ruff-0.15.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", size = 10693187, upload-time = "2026-06-18T18:25:08.245Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fd/44/ff6c635cf2c4f4e7b618b6640da057376baa36014695487d88aed4794268/ruff-0.15.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", size = 11208721, upload-time = "2026-06-18T18:25:41.327Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/88/d9/5baa2a30861adfb7022cf33c1e35b2fc18085b08c16f83eff4c7b99a5f48/ruff-0.15.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", size = 11678599, upload-time = "2026-06-18T18:25:13.607Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c3/1a/0725a7cfdc32ff769efb96ee782bec882e16448c5d9e3be947ec4c04ce27/ruff-0.15.18-py3-none-win32.whl", hash = "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", size = 10901903, upload-time = "2026-06-18T18:25:24.755Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f3/51/805d9f6fb7970505c3504794a5ec350f605361b807fef4dcf214ebd35e72/ruff-0.15.18-py3-none-win_amd64.whl", hash = "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3", size = 12041189, upload-time = "2026-06-18T18:25:17.915Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" }, ] [[package]] name = "s3transfer" version = "0.19.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/94/dcdaeb1713cab9c84def276cfac7388b17c7d9855bbcfe88d77e4dbafd44/s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685", size = 165171, upload-time = "2026-06-16T19:44:51.599Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f6/94/dcdaeb1713cab9c84def276cfac7388b17c7d9855bbcfe88d77e4dbafd44/s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685", size = 165171, upload-time = "2026-06-16T19:44:51.599Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/5f/4c174edad94f82de888ac00a5ddd8d07b35609b6c94f0bdf4d74af57703e/s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262", size = 90101, upload-time = "2026-06-16T19:44:50.439Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/46/5f/4c174edad94f82de888ac00a5ddd8d07b35609b6c94f0bdf4d74af57703e/s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262", size = 90101, upload-time = "2026-06-16T19:44:50.439Z" }, ] [[package]] name = "safety" version = "3.8.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "authlib" }, { name = "certifi" }, @@ -4388,15 +4636,15 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/7b/8e1d580c5178f0736b806b7199827e61e2a2569eec5b49ec75da6273bbdf/safety-3.8.1.tar.gz", hash = "sha256:e646123b976bbb6707cfaacae8c926e2f886b744a60e0f410e8610a3a4eaf7be", size = 412947, upload-time = "2026-05-29T15:09:33.355Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/7b/8e1d580c5178f0736b806b7199827e61e2a2569eec5b49ec75da6273bbdf/safety-3.8.1.tar.gz", hash = "sha256:e646123b976bbb6707cfaacae8c926e2f886b744a60e0f410e8610a3a4eaf7be", size = 412947, upload-time = "2026-05-29T15:09:33.355Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/f5/498db84333a644835e572c0d96cfa705bf9871f863289a7845082d54755e/safety-3.8.1-py3-none-any.whl", hash = "sha256:953c1c3c60c873f53a6cc250b2a9c4b38bb6ef45f0625990e43f20bff916c965", size = 340521, upload-time = "2026-05-29T15:09:31.622Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4b/f5/498db84333a644835e572c0d96cfa705bf9871f863289a7845082d54755e/safety-3.8.1-py3-none-any.whl", hash = "sha256:953c1c3c60c873f53a6cc250b2a9c4b38bb6ef45f0625990e43f20bff916c965", size = 340521, upload-time = "2026-05-29T15:09:31.622Z" }, ] [[package]] name = "safety-schemas" version = "0.0.16" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "dparse" }, { name = "packaging" }, @@ -4404,296 +4652,375 @@ dependencies = [ { name = "ruamel-yaml" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/ef/0e07dfdb4104c4e42ae9fc6e8a0da7be2d72ac2ee198b32f7500796de8f3/safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e", size = 54815, upload-time = "2025-09-16T14:35:31.973Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c2/ef/0e07dfdb4104c4e42ae9fc6e8a0da7be2d72ac2ee198b32f7500796de8f3/safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e", size = 54815, upload-time = "2025-09-16T14:35:31.973Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/a2/7840cc32890ce4b84668d3d9dfe15a48355b683ae3fb627ac97ac5a4265f/safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44", size = 39292, upload-time = "2025-09-16T14:35:32.84Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/84/a2/7840cc32890ce4b84668d3d9dfe15a48355b683ae3fb627ac97ac5a4265f/safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44", size = 39292, upload-time = "2025-09-16T14:35:32.84Z" }, ] [[package]] name = "sarif-om" version = "1.0.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "attrs" }, { name = "pbr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/de/bbdd93fe456d4011500784657c5e4a31e3f4fcbb276255d4db1213aed78c/sarif_om-1.0.4.tar.gz", hash = "sha256:cd5f416b3083e00d402a92e449a7ff67af46f11241073eea0461802a3b5aef98", size = 28847, upload-time = "2019-10-05T20:11:23.338Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ba/de/bbdd93fe456d4011500784657c5e4a31e3f4fcbb276255d4db1213aed78c/sarif_om-1.0.4.tar.gz", hash = "sha256:cd5f416b3083e00d402a92e449a7ff67af46f11241073eea0461802a3b5aef98", size = 28847, upload-time = "2019-10-05T20:11:23.338Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/7c/1d3d0467565aa8b3e77ab8712042a09dd1158056826f45783f3d2b34adf1/sarif_om-1.0.4-py3-none-any.whl", hash = "sha256:539ef47a662329b1c8502388ad92457425e95dc0aaaf995fe46f4984c4771911", size = 30193, upload-time = "2019-10-05T20:11:21.577Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/82/7c/1d3d0467565aa8b3e77ab8712042a09dd1158056826f45783f3d2b34adf1/sarif_om-1.0.4-py3-none-any.whl", hash = "sha256:539ef47a662329b1c8502388ad92457425e95dc0aaaf995fe46f4984c4771911", size = 30193, upload-time = "2019-10-05T20:11:21.577Z" }, ] [[package]] name = "secretstorage" version = "3.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "cryptography" }, { name = "jeepney" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, ] [[package]] name = "semgrep" -version = "1.79.0" -source = { registry = "https://pypi.org/simple" } +version = "1.167.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "attrs" }, { name = "boltons" }, { name = "click" }, { name = "click-option-group" }, { name = "colorama" }, - { name = "defusedxml" }, { name = "exceptiongroup" }, { name = "glom" }, { name = "jsonschema" }, + { name = "mcp" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation-requests" }, + { name = "opentelemetry-instrumentation-threading" }, + { name = "opentelemetry-sdk" }, { name = "packaging" }, { name = "peewee" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "requests" }, { name = "rich" }, { name = "ruamel-yaml" }, + { name = "ruamel-yaml-clib" }, + { name = "semantic-version" }, { name = "tomli" }, { name = "typing-extensions" }, { name = "urllib3" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/61/9ee9e601ddc9f9073708d4e6886d0c7021b59b3180b6cb53c0bd01b393d9/semgrep-1.79.0.tar.gz", hash = "sha256:fde15d090b4beb865e12c2c727404c8dee2f41b9d793a7f972b278cdefb22bea", size = 27421587, upload-time = "2024-07-10T10:06:05.122Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/33/cb/5c694b392583aa73727272ae48aecf28703ba98d4aa1dd96f7bf2ac2b0d6/semgrep-1.167.0.tar.gz", hash = "sha256:7ee779321c3a8580ba1192d4933422ae29f8802ca38edcbfa942ce0a97592ac1", size = 56149438, upload-time = "2026-06-17T18:23:54.505Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/4a/469abc30b134354632d8b8e83249121447fca74a615509a09bed90340813/semgrep-1.79.0-cp38.cp39.cp310.cp311.py37.py38.py39.py310.py311-none-any.whl", hash = "sha256:5a28858c1f5249bf4fed3f180f2db2589ce1832c1058eb6d18a0f086c91dadc1", size = 27832465, upload-time = "2024-07-10T10:05:45.254Z" }, - { url = "https://files.pythonhosted.org/packages/e9/23/eff37582f900cf742b5fc7709dfd0e63cc2bc0faefe6ec200f150666fa7a/semgrep-1.79.0-cp38.cp39.cp310.cp311.py37.py38.py39.py310.py311-none-macosx_10_14_x86_64.whl", hash = "sha256:4b22b5f4db17204648baf8bc58fcd74c09eb5f41bd407422193253b4de9af18e", size = 28050871, upload-time = "2024-07-10T10:05:52.086Z" }, - { url = "https://files.pythonhosted.org/packages/d6/88/35615a4e1142755cb3d2c86d63160f63ab770c111c82de9318bba27fb888/semgrep-1.79.0-cp38.cp39.cp310.cp311.py37.py38.py39.py310.py311-none-macosx_11_0_arm64.whl", hash = "sha256:fe5cf0ac8afdb786cbd4e6c97c418ca7adc8f4c42584a69dc7c10e15db5f7d9b", size = 33805877, upload-time = "2024-07-10T10:05:56.42Z" }, - { url = "https://files.pythonhosted.org/packages/fd/84/3b6afc829f54b331f47d55c565096ef74a98d96d794612d2e5330062d373/semgrep-1.79.0-cp38.cp39.cp310.cp311.py37.py38.py39.py310.py311-none-musllinux_1_0_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41797931371d05c41a6e09861b3d631136b4fe644d80802b2fd48af650e5fa5a", size = 32479030, upload-time = "2024-07-10T10:06:00.777Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d7/f8/93423330211b987ad6ce17d9fb54cb3772a02c28fb2bb9e302f7be5164b1/semgrep-1.167.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_10_14_x86_64.whl", hash = "sha256:7d8b222f13a947c164e6b89e64258474c0fd59c08bfd1071ed7b3216afcd8cf6", size = 45184663, upload-time = "2026-06-17T18:23:24.528Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/08/0b/ac9ff80fa5919f5048fb0005ff7c578f6d0543ee5080cdab564a625d919d/semgrep-1.167.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:ca5ba16bd2bf7a3e305a32ad292f66246947d31c41603b580453acd7ab859827", size = 49140453, upload-time = "2026-06-17T18:23:28.554Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/08/de/e72a0e28174cb34a4fbe0d9d5bab805d16683dae6c3eb71bdf06f3f0f15a/semgrep-1.167.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_aarch64.whl", hash = "sha256:a4912d2af1371ad96524163ec10842227fcc79dd14fd8b6a706d598bf380fcbc", size = 71142408, upload-time = "2026-06-17T18:23:32.685Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/0a/f287be6aff5a2d10528df9e985f8a630fc552960b4e966a21e3edb7d705c/semgrep-1.167.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_x86_64.whl", hash = "sha256:687f08c3c3951bcc6f67d63cc0019c40401c900ecc26a6e56382b6cc02f4f50f", size = 69012140, upload-time = "2026-06-17T18:23:37.386Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6c/bd/95b42a821ff4eb15887716591cd472afb7e3003761d3b43ad6a8e833fd6d/semgrep-1.167.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_aarch64.whl", hash = "sha256:e7d82cc6b5eb8bd699b2a329e6c11de7ba039643c69c1b61150cda1298585aee", size = 79105757, upload-time = "2026-06-17T18:23:41.883Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/99/c7/d3affb19e16180b034363539f06a274b94df58b10940c6c0c97bc8b90eeb/semgrep-1.167.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_x86_64.whl", hash = "sha256:912be28e47409bb1c53d1a4512f2a39ac9e693c7895cb94d8c181702c8bed837", size = 76605570, upload-time = "2026-06-17T18:23:46.376Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/38/7e/203e7dbea7c1f534eedce440216c7c77151a1dab96c4b02b16edff5b3337/semgrep-1.167.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:b01823d9b540cccb92e09a404ee796f5fe2a28c132170967386909e3b77f5b8e", size = 57083314, upload-time = "2026-06-17T18:23:50.727Z" }, ] [[package]] name = "semver" version = "3.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, ] [[package]] name = "setuptools" version = "82.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, ] [[package]] name = "shellingham" version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "six" version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "smmap" version = "5.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, ] [[package]] name = "sortedcontainers" version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] name = "sqlalchemy" version = "2.0.51" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/76/b3ea1d8842e7b62c718a88d302809003d65ed82011460ca48907dde658c4/sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0", size = 2162087, upload-time = "2026-06-15T16:05:15.795Z" }, - { url = "https://files.pythonhosted.org/packages/6c/22/f19552eb7876774d50cfd025337ef5d67acc10cd8f29adab7716cf47c352/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652", size = 3244579, upload-time = "2026-06-15T16:10:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/e4a2eb5a8ec5cd3c2a0615a2f15f0afca89ac039229599b9ed0c0ed28e5e/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d", size = 3243515, upload-time = "2026-06-15T16:12:22.627Z" }, - { url = "https://files.pythonhosted.org/packages/74/c6/5900ec624fab3360aa2ec59b99bb2046dd79799e310bb78a0514eaa4038e/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84", size = 3195492, upload-time = "2026-06-15T16:10:38.097Z" }, - { url = "https://files.pythonhosted.org/packages/8f/41/2ee3c4e1ac4fd22309349823fe13f33febeab1a71db1d7e9d60293a07dcb/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080", size = 3215782, upload-time = "2026-06-15T16:12:24.051Z" }, - { url = "https://files.pythonhosted.org/packages/ce/1c/3bd72c341f1cb5faed5a7457ea840228a46be51cfbaf31a9db72fc963f11/sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1", size = 2122119, upload-time = "2026-06-15T16:13:26.915Z" }, - { url = "https://files.pythonhosted.org/packages/2a/63/b6dfdd646abf91c3bedb13727226a5e765e5f8365e898d43818e6672fa46/sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a", size = 2145158, upload-time = "2026-06-15T16:13:28.386Z" }, - { url = "https://files.pythonhosted.org/packages/3a/69/a67c69e5f28fc9c99d6f7bd60bd50e91f2fed2423e3b30fb228fa00e51f3/sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba", size = 2161838, upload-time = "2026-06-15T16:05:17.144Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/c8c22b8438bddc0a030157c6ec0f6ef97b3c38effa444bdab2a27af04090/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604", size = 3319402, upload-time = "2026-06-15T16:10:40.002Z" }, - { url = "https://files.pythonhosted.org/packages/90/54/44012d32fd77d991256d2ff793ba3807c51d40cb27a85b4796224f6744df/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd", size = 3319675, upload-time = "2026-06-15T16:12:25.658Z" }, - { url = "https://files.pythonhosted.org/packages/29/a5/de0592acaf5906cd7430874392d6f7e8b4a7c8437610953ee2d1501c0b44/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260", size = 3270777, upload-time = "2026-06-15T16:10:42.125Z" }, - { url = "https://files.pythonhosted.org/packages/cb/14/a44c90739c780b362238e4ac3cb19dd0ca40d13e6ddc5daa112166ddab4f/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265", size = 3293940, upload-time = "2026-06-15T16:12:27.156Z" }, - { url = "https://files.pythonhosted.org/packages/65/eb/fbd0f206a330e66f8c602a99c37c4e731f107faed62954b41b01f16dd9d9/sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86", size = 2121183, upload-time = "2026-06-15T16:13:29.905Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fd/005bf80f3cf6e5c62b5dd68616280f51cd012c60840fa74781b3ed7b1623/sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc", size = 2145796, upload-time = "2026-06-15T16:13:31.283Z" }, - { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, - { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, - { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, - { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, - { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, - { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, - { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, - { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, - { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, - { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, - { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, - { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, - { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, - { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, - { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, - { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, - { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/71/76/b3ea1d8842e7b62c718a88d302809003d65ed82011460ca48907dde658c4/sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0", size = 2162087, upload-time = "2026-06-15T16:05:15.795Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6c/22/f19552eb7876774d50cfd025337ef5d67acc10cd8f29adab7716cf47c352/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652", size = 3244579, upload-time = "2026-06-15T16:10:36.165Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fc/97/e4a2eb5a8ec5cd3c2a0615a2f15f0afca89ac039229599b9ed0c0ed28e5e/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d", size = 3243515, upload-time = "2026-06-15T16:12:22.627Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/74/c6/5900ec624fab3360aa2ec59b99bb2046dd79799e310bb78a0514eaa4038e/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84", size = 3195492, upload-time = "2026-06-15T16:10:38.097Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8f/41/2ee3c4e1ac4fd22309349823fe13f33febeab1a71db1d7e9d60293a07dcb/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080", size = 3215782, upload-time = "2026-06-15T16:12:24.051Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ce/1c/3bd72c341f1cb5faed5a7457ea840228a46be51cfbaf31a9db72fc963f11/sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1", size = 2122119, upload-time = "2026-06-15T16:13:26.915Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2a/63/b6dfdd646abf91c3bedb13727226a5e765e5f8365e898d43818e6672fa46/sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a", size = 2145158, upload-time = "2026-06-15T16:13:28.386Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3a/69/a67c69e5f28fc9c99d6f7bd60bd50e91f2fed2423e3b30fb228fa00e51f3/sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba", size = 2161838, upload-time = "2026-06-15T16:05:17.144Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9a/a4/c8c22b8438bddc0a030157c6ec0f6ef97b3c38effa444bdab2a27af04090/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604", size = 3319402, upload-time = "2026-06-15T16:10:40.002Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/90/54/44012d32fd77d991256d2ff793ba3807c51d40cb27a85b4796224f6744df/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd", size = 3319675, upload-time = "2026-06-15T16:12:25.658Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/29/a5/de0592acaf5906cd7430874392d6f7e8b4a7c8437610953ee2d1501c0b44/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260", size = 3270777, upload-time = "2026-06-15T16:10:42.125Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cb/14/a44c90739c780b362238e4ac3cb19dd0ca40d13e6ddc5daa112166ddab4f/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265", size = 3293940, upload-time = "2026-06-15T16:12:27.156Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/65/eb/fbd0f206a330e66f8c602a99c37c4e731f107faed62954b41b01f16dd9d9/sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86", size = 2121183, upload-time = "2026-06-15T16:13:29.905Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ad/fd/005bf80f3cf6e5c62b5dd68616280f51cd012c60840fa74781b3ed7b1623/sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc", size = 2145796, upload-time = "2026-06-15T16:13:31.283Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.4.5" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, ] [[package]] name = "stack-data" version = "0.6.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "asttokens" }, { name = "executing" }, { name = "pure-eval" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] [[package]] name = "starlette" version = "1.3.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] [[package]] name = "stevedore" version = "5.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/88/35e4d27d9177d7df76d060e0a18f69c6c5794c96960c94042e20a12c8ba2/stevedore-5.8.0.tar.gz", hash = "sha256:b49867b32ca3016e94100e68dbf26e72aa7b8708d0a3f73c08aeb220370ac715", size = 514710, upload-time = "2026-05-18T09:15:27.731Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e9/88/35e4d27d9177d7df76d060e0a18f69c6c5794c96960c94042e20a12c8ba2/stevedore-5.8.0.tar.gz", hash = "sha256:b49867b32ca3016e94100e68dbf26e72aa7b8708d0a3f73c08aeb220370ac715", size = 514710, upload-time = "2026-05-18T09:15:27.731Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/ac/19f9941c74add59d17694930ec8105d5eddeee4ce56dd8632b765ca16d6c/stevedore-5.8.0-py3-none-any.whl", hash = "sha256:88eede9e66ca80e34085b9174e2327da2c61ac91f24f70e41c3ad76e4bb4872b", size = 54553, upload-time = "2026-05-18T09:15:25.82Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f5/ac/19f9941c74add59d17694930ec8105d5eddeee4ce56dd8632b765ca16d6c/stevedore-5.8.0-py3-none-any.whl", hash = "sha256:88eede9e66ca80e34085b9174e2327da2c61ac91f24f70e41c3ad76e4bb4872b", size = 54553, upload-time = "2026-05-18T09:15:25.82Z" }, ] [[package]] name = "sympy" version = "1.14.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "mpmath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] [[package]] name = "tenacity" version = "9.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, -] - -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] [[package]] name = "tomli" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096, upload-time = "2024-10-02T10:46:13.208Z" } +version = "2.4.1" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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 = "tomli-w" +version = "1.2.0" +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237, upload-time = "2024-10-02T10:46:11.806Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] [[package]] name = "tomlkit" version = "0.13.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, ] [[package]] name = "tqdm" version = "4.68.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, ] [[package]] name = "traitlets" version = "5.15.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, ] [[package]] name = "truststore" version = "0.10.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, ] [[package]] name = "twine" version = "6.2.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "id" }, { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, @@ -4705,119 +5032,119 @@ dependencies = [ { name = "rich" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, ] [[package]] name = "typer" version = "0.23.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "annotated-doc" }, { name = "click" }, { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, ] [[package]] name = "types-python-dateutil" version = "2.9.0.20260518" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/e8/c01bdf0d7c3659428c091fbd693177093639565bcbc86bc20098e6d37cc6/types_python_dateutil-2.9.0.20260518.tar.gz", hash = "sha256:51f02dc03b61c7f6a07df45797d4dfe8a1aa47f0b7db9ad89f6fd3a1a70e1b51", size = 17082, upload-time = "2026-05-18T06:05:24.508Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8d/e8/c01bdf0d7c3659428c091fbd693177093639565bcbc86bc20098e6d37cc6/types_python_dateutil-2.9.0.20260518.tar.gz", hash = "sha256:51f02dc03b61c7f6a07df45797d4dfe8a1aa47f0b7db9ad89f6fd3a1a70e1b51", size = 17082, upload-time = "2026-05-18T06:05:24.508Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/22/169273273ca34e9ab0ae2f387ba72ed7e09faaaf834da01d6b89c2bea71a/types_python_dateutil-2.9.0.20260518-py3-none-any.whl", hash = "sha256:d6a9c5bd0de61460c8fdef8ab2b400f956a1a1075cce08d4e2b4434e478c50b8", size = 18431, upload-time = "2026-05-18T06:05:23.641Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/36/22/169273273ca34e9ab0ae2f387ba72ed7e09faaaf834da01d6b89c2bea71a/types_python_dateutil-2.9.0.20260518-py3-none-any.whl", hash = "sha256:d6a9c5bd0de61460c8fdef8ab2b400f956a1a1075cce08d4e2b4434e478c50b8", size = 18431, upload-time = "2026-05-18T06:05:23.641Z" }, ] [[package]] name = "types-pyyaml" version = "6.0.12.20260518" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } 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" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } 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" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/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 = "tzdata" version = "2026.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] [[package]] name = "uri-template" version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, ] [[package]] name = "urllib3" version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "uvicorn" version = "0.49.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, ] [[package]] name = "verspec" version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, ] [[package]] name = "virtualenv" version = "21.5.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "distlib" }, { name = "filelock" }, @@ -4825,180 +5152,180 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" }, ] [[package]] name = "watchdog" version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] [[package]] name = "wcmatch" version = "8.5.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "bracex" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/c4/55e0d36da61d7b8b2a49fd273e6b296fd5e8471c72ebbe438635d1af3968/wcmatch-8.5.2.tar.gz", hash = "sha256:a70222b86dea82fb382dd87b73278c10756c138bd6f8f714e2183128887b9eb2", size = 114983, upload-time = "2024-05-15T12:51:08.054Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ea/c4/55e0d36da61d7b8b2a49fd273e6b296fd5e8471c72ebbe438635d1af3968/wcmatch-8.5.2.tar.gz", hash = "sha256:a70222b86dea82fb382dd87b73278c10756c138bd6f8f714e2183128887b9eb2", size = 114983, upload-time = "2024-05-15T12:51:08.054Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/78/533ef890536e5ba0fd4f7df37482b5800ecaaceae9afc30978a1a7f88ff1/wcmatch-8.5.2-py3-none-any.whl", hash = "sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478", size = 39397, upload-time = "2024-05-15T12:51:06.2Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/09/78/533ef890536e5ba0fd4f7df37482b5800ecaaceae9afc30978a1a7f88ff1/wcmatch-8.5.2-py3-none-any.whl", hash = "sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478", size = 39397, upload-time = "2024-05-15T12:51:06.2Z" }, ] [[package]] name = "wcwidth" version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/b4/51fe890511f0f242d07cb1ebe6a5b6db417262b9d2568b460347c57d95cc/wcwidth-0.8.1.tar.gz", hash = "sha256:faf5b4a5366a72dc49cad48cdf21f52bdf63bdda995178e483ba247ff79089b9", size = 1466072, upload-time = "2026-06-08T05:57:23.146Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/49/b4/51fe890511f0f242d07cb1ebe6a5b6db417262b9d2568b460347c57d95cc/wcwidth-0.8.1.tar.gz", hash = "sha256:faf5b4a5366a72dc49cad48cdf21f52bdf63bdda995178e483ba247ff79089b9", size = 1466072, upload-time = "2026-06-08T05:57:23.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl", hash = "sha256:f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8", size = 323092, upload-time = "2026-06-08T05:57:21.413Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl", hash = "sha256:f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8", size = 323092, upload-time = "2026-06-08T05:57:21.413Z" }, ] [[package]] name = "webcolors" version = "25.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, ] [[package]] name = "werkzeug" version = "3.1.8" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, ] [[package]] name = "wheel" version = "0.47.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, ] [[package]] name = "wrapt" version = "1.17.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, - { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, - { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, - { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, - { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, - { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, - { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, - { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, - { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, - { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, - { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, - { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] [[package]] name = "xmltodict" version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61", size = 26124, upload-time = "2026-02-22T02:21:22.074Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61", size = 26124, upload-time = "2026-02-22T02:21:22.074Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, ] [[package]] name = "zipp" version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +source = { registry = "https://pkgs.safetycli.com/repository/fgo-inc/pypi/simple/" } +sdist = { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, + { url = "https://pkgs.safetycli.com/package/fgo-inc/pypi/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, ]