diff --git a/CLAUDE.md b/CLAUDE.md index e68ad32..e3907c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ Every helper is extracted from a real consumer, not speculated. | Submodule | What it is | |---|---| | `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware (KYC, age, sanctions, jurisdiction) | -| `agentscore_commerce.payment` | Networks/USDC/rails registries, paymentauth.org directive builders, `create_x402_server` (wraps `x402[evm]>=2.9` + `cdp-sdk` for `facilitator="coinbase"`; install via the `coinbase` extra), `build_x402_accepts_for_402` (build the 402's `accepts[]` from the registered scheme — derives the right `extra.name` per network), `process_x402_settle` (verify+settle in one call), `create_mppx_server` (wraps `pympp[server,tempo,stripe]>=0.6`), dispatch-by-network, signer extraction, WWW-Authenticate header, Settlement-Overrides header | +| `agentscore_commerce.payment` | Networks/USDC/rails registries, paymentauth.org directive builders, `create_x402_server` (wraps `x402[evm]>=2.9` + `cdp-sdk` for `facilitator="coinbase"`; install via the `coinbase` extra), `build_x402_accepts_for_402` (build the 402's `accepts[]` from the registered scheme; derives the right `extra.name` per network), `process_x402_settle` (verify+settle in one call), `create_mppx_server` (wraps `pympp[server,tempo,stripe]>=0.6`), dispatch-by-network, signer extraction, WWW-Authenticate header, Settlement-Overrides header | | `agentscore_commerce.discovery` | Discovery probe, Bazaar wrapper, `/.well-known/mpp.json`, `llms.txt` builder, `skill.md` builder (Claude-Skill-compatible agent-discovery manifest), OpenAPI snippets, `NoindexNonDiscoveryMiddleware` ASGI middleware | | `agentscore_commerce.challenge` | 402-body builders: accepted_methods, identity_metadata, how_to_pay, agent_instructions, build_402_body, `build_validation_error` (4xx body builder) | | `agentscore_commerce.stripe_multichain` | Multichain PaymentIntent helper, deposit-address lookup, testnet simulator, mppx Stripe wrapper | @@ -17,7 +17,7 @@ Every helper is extracted from a real consumer, not speculated. ## Architecture -Single Python package, hatchling-built, published to PyPI as `agentscore-commerce`. Per-framework identity adapters expose the same surface — `AgentScoreGate` (or `agentscore_gate(app, ...)` for Flask/Sanic), `capture_wallet`, `verify_wallet_signer_match`, `get_assess_data`, `get_gate_degraded_state`, `get_gate_quota_info` — with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim). +Single Python package, hatchling-built, published to PyPI as `agentscore-commerce`. Per-framework identity adapters expose the same surface (`AgentScoreGate`, or `agentscore_gate(app, ...)` for Flask/Sanic; `capture_wallet`, `verify_wallet_signer_match`, `get_assess_data`, `get_gate_degraded_state`, `get_gate_quota_info`) with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim). | Directory | Contents | |---|---| @@ -30,11 +30,11 @@ Single Python package, hatchling-built, published to PyPI as `agentscore-commerc | `examples/` | Runnable single-file FastAPI apps for each common scenario | | `tests/` | pytest, one file per surface | -Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime — vendors install only what they use via extras (`pip install agentscore-commerce[fastapi,stripe]` etc.). Underlying packages: `x402[evm]`, `pympp[server,tempo,stripe]`, `stripe`, `cdp-sdk` (the `coinbase` extra — only needed when `facilitator="coinbase"`). Missing peer dep raises a guiding `ImportError` with the install command. +Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime; vendors install only what they use via extras (`pip install agentscore-commerce[fastapi,stripe]` etc.). Underlying packages: `x402[evm]`, `pympp[server,tempo,stripe]`, `stripe`, `cdp-sdk` (the `coinbase` extra; only needed when `facilitator="coinbase"`). Missing peer dep raises a guiding `ImportError` with the install command. ## Examples -`examples/` contains full single-file FastAPI apps for the most common merchant scenarios — copy-paste templates, not frameworks: +`examples/` contains full single-file FastAPI apps for the most common merchant scenarios; copy-paste templates, not frameworks: | Example | Scenario | |---|---| @@ -43,28 +43,29 @@ Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime — | `multi_rail_merchant.py` | Full agent-commerce: identity + Tempo MPP + x402 + Stripe SPT | | `stripe_multichain_merchant.py` | Stripe-anchored multichain (PaymentIntent → tempo/base/solana deposit addresses) | | `variable_cost_merchant.py` | Pay-per-actual-usage on **two protocols**: x402 upto (Permit2 + Settlement-Overrides) AND MPP tempo session (channel + SSE + mid-stream vouchers) | -| `compliance_merchant.py` | Regulated-goods merchant — full compliance gate + custom `on_denied` composing the denial helpers (`verification_agent_instructions`, `is_fixable_denial`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `denial_reason_to_body`/`denial_reason_status`) | +| `compliance_merchant.py` | Regulated-goods merchant: full compliance gate + custom `on_denied` composing the denial helpers (`verification_agent_instructions`, `is_fixable_denial`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `denial_reason_to_body`/`denial_reason_status`) | | `per_product_policy_merchant.py` | Multi-product merchant where each row carries its own compliance policy. One product hard-gates KYC + age + state; another is anonymous; a third uses `enforcement="soft"` (request KYC but don't block sale). Demonstrates `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`. | +| `signed_ucp_merchant.py` | Signed UCP profile (`/.well-known/ucp`) + JWKS endpoint (`/.well-known/jwks.json`). AgentScore's `agentscore-profile+jws` is a vendor extension on top of UCP for trust-mode verifiers (Visa AP2 pilots, regulated-commerce verifiers) that opt into auditable cryptographic provenance — UCP §6 itself does NOT mandate signing; Pura Vida and other Shopify-backed UCP merchants ship unsigned in production. Wires ephemeral-for-dev / env-JWK-for-prod signing, kid rotation, and `Cache-Control` posture. Uses `generate_ucp_signing_key`, `sign_ucp_profile`, `build_jwks_response`, `UCPSigningKey.from_jwk`, `UCPVerificationError`. | ## Identity model -Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator-Token`). Default checks operator-token first, then wallet. Address normalization is network-aware via `agentscore_commerce/identity/address.py`: EVM lowercased, Solana base58 preserved verbatim — used for cache keys, wallet→operator resolves, and signer-match comparisons. +Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator-Token`). Default checks operator-token first, then wallet. Address normalization is network-aware via `agentscore_commerce/identity/address.py`: EVM lowercased, Solana base58 preserved verbatim. Used for cache keys, wallet→operator resolves, and signer-match comparisons. `DenialReason` codes (`missing_identity`, `identity_verification_required`, `token_expired`, `invalid_credential`, `wallet_signer_mismatch`, `wallet_auth_requires_wallet_signing`, `wallet_not_trusted`, `api_error`, `payment_required`) each carry a structured `agent_instructions` JSON block describing concrete recovery actions. See `agentscore_commerce/identity/_response.py` for the canned action copy. `create_session_on_missing` auto-mints a verification session when no identity is present and returns 403 with `verify_url` + poll instructions. `verify_wallet_signer_match` (per-adapter) compares the recovered signer against `linked_wallets[]` for cross-chain wallet-stack matching. -Captured wallets: `capture_wallet(...)` is fire-and-forget — reads `operator_token` stashed during gating and POSTs to `/v1/credentials/wallets`. No-ops for wallet-authenticated requests. +Captured wallets: `capture_wallet(...)` is fire-and-forget. Reads `operator_token` stashed during gating and POSTs to `/v1/credentials/wallets`. No-ops for wallet-authenticated requests. -Wallet-signer-match: `verify_wallet_signer_match` / `averify_wallet_signer_match` makes a single `/v1/assess` call with `resolve_signer` set; the API resolves both wallets and emits a `signer_match` verdict in the same response — collapses the legacy 2 follow-up assess calls into one round trip. Repeat lookups for the same `(claimed, signer)` pair hit a per-cache-entry `signer_match_by_signer` sub-dict and skip the API entirely. Falls back to a 2-resolve path when the API doesn't emit `signer_match` (canary rollout safety). +Wallet-signer-match: `verify_wallet_signer_match` / `averify_wallet_signer_match` makes a single `/v1/assess` call with `resolve_signer` set; the API resolves both wallets and emits a `signer_match` verdict in the same response, collapsing the legacy 2 follow-up assess calls into one round trip. Repeat lookups for the same `(claimed, signer)` pair hit a per-cache-entry `signer_match_by_signer` sub-dict and skip the API entirely. Falls back to a 2-resolve path when the API doesn't emit `signer_match` (canary rollout safety). ### Fail-open (opt-in) -`fail_open=True` on `AgentScoreGate(...)` (or `agentscore_gate(app, ...)`) flips infra-failure handling: 429 / 5xx / network-timeout pass through to the handler with the gate state stamped `degraded=True` + `infra_reason="quota_exceeded" | "api_error" | "network_timeout"`. `get_gate_degraded_state(request)` (Flask: `get_gate_degraded_state()` — reads from `g`) returns `{"degraded": bool, "infra_reason"?: str}` for merchant logging/alerting. Default stays `fail_open=False` — regulated commerce should keep it. Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of the flag. The gate's `try` wraps only the AgentScore call — never the downstream user handler. +`fail_open=True` on `AgentScoreGate(...)` (or `agentscore_gate(app, ...)`) flips infra-failure handling: 429 / 5xx / network-timeout pass through to the handler with the gate state stamped `degraded=True` + `infra_reason="quota_exceeded" | "api_error" | "network_timeout"`. `get_gate_degraded_state(request)` (Flask: `get_gate_degraded_state()`, reads from `g`) returns `{"degraded": bool, "infra_reason"?: str}` for merchant logging/alerting. Default stays `fail_open=False`; regulated commerce should keep it. Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of the flag. The gate's `try` wraps only the AgentScore call, never the downstream user handler. ### Mount posture: gate-first vs gate-conditional -`AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic) is mounted directly when the route is AgentScore-only — every request runs identity + policy. To support **anonymous discovery by any spec-compliant x402 wallet** (Coinbase awal, Phantom, Solflare, …), wrap the gate so it fires only when a payment credential is attached: +`AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic) is mounted directly when the route is AgentScore-only; every request runs identity + policy. To support **anonymous discovery by any spec-compliant x402 wallet** (Coinbase awal, Phantom, Solflare, ...), wrap the gate so it fires only when a payment credential is attached: ```python _gate = AgentScoreGate(api_key=..., require_kyc=True, ...) @@ -87,20 +88,20 @@ Anonymous POST flows through to the handler unauthenticated and gets a 402 with ### `compatible_clients` field on emitted 402s -`build_agent_instructions` emits a `compatible_clients` field in the 402 body, derived automatically from `how_to_pay` — per-rail list of CLIs the AgentScore team has smoke-verified end-to-end. Vendors override with `BuildAgentInstructionsInput(compatible_clients={...})` to add their own tested clients. Set to an empty dict `{}` to suppress the default. Same data is published as `core/docs/integrations/x402-clients.mdx` for human-side rationale + per-rail commands. +`build_agent_instructions` emits a `compatible_clients` field in the 402 body, derived automatically from `how_to_pay`: per-rail list of CLIs the AgentScore team has smoke-verified end-to-end. Vendors override with `BuildAgentInstructionsInput(compatible_clients={...})` to add their own tested clients. Set to an empty dict `{}` to suppress the default. Same data is published as `core/docs/integrations/x402-clients.mdx` for human-side rationale + per-rail commands. ## Tooling -- **uv** — package manager. -- **ruff** — linting + formatting. -- **ty** — type checker (Astral). -- **vulture** — dead code detection. -- **pytest** — tests. -- **Lefthook** — pre-commit ruff, pre-push ty + vulture (parallel). +- **uv**: package manager. +- **ruff**: linting + formatting. +- **ty**: type checker (Astral). +- **vulture**: dead code detection. +- **pytest**: tests. +- **Lefthook**: pre-commit ruff, pre-push ty + vulture (parallel). ```bash uv sync --all-extras -uv run lefthook install # one-time per clone — wires pre-commit + pre-push +uv run lefthook install # one-time per clone; wires pre-commit + pre-push uv run ruff check . uv run ruff format . uv run ty check agentscore_commerce/ @@ -112,16 +113,16 @@ uv run pytest tests/ 1. Create a branch 2. Make changes 3. Lefthook runs ruff on commit, ty + vulture on push -4. Open a PR — CI runs automatically +4. Open a PR (CI runs automatically) 5. Merge (squash) ## Rules - **No silent refactors** - **Never commit .env files or secrets** -- **Use PRs** — never push directly to main +- **Use PRs**: never push directly to main - **Helpers are protocol translations + configurable opinions, not opinionated frameworks** -- **Cross-language API parity** — keep the surface area identical between the node and python flavors so vendors switching languages have the same mental model +- **Cross-language API parity**: keep the surface area identical between the node and python flavors so vendors switching languages have the same mental model ## Releasing diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b59db24..f2353d8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ Thanks for your interest in contributing! Here's how to get started. - All PRs require 1 approval before merging - Squash merge to `main` is the standard -- Keep PRs focused — one feature or fix per PR +- Keep PRs focused: one feature or fix per PR - Include tests for new functionality - Make sure CI passes before requesting review diff --git a/README.md b/README.md index 33cd918..a3e7d4a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://img.shields.io/pypi/v/agentscore-commerce.svg)](https://pypi.org/project/agentscore-commerce/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -The full merchant-side SDK for [AgentScore](https://agentscore.sh) in Python — agent commerce in one install. Identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI), payment helpers, 402 challenge builders, MPP discovery, and Stripe multichain support. +The full merchant-side SDK for [AgentScore](https://agentscore.sh) in Python: agent commerce in one install. Identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI), payment helpers, 402 challenge builders, MPP discovery, and Stripe multichain support. ## Install @@ -25,12 +25,12 @@ pip install 'agentscore-commerce[fastapi,x402,coinbase]' | Submodule | What it provides | |---|---| | `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware: KYC, sanctions, age, jurisdiction. `AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic), `get_assess_data(...)`, `capture_wallet(...)`, `verify_wallet_signer_match(...)`. | -| `agentscore_commerce.identity` (package level) | Re-exports the denial helpers: `denial_reason_status`, `denial_reason_to_body`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `verification_agent_instructions`, `is_fixable_denial`, `FIXABLE_DENIAL_REASONS`. Also re-exports the per-product policy helpers: `PolicyBlock`, `GateResult`, `EnforcementMode`, `IdentityStatus`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed` — for multi-product merchants where each product carries its own compliance config (hard gate vs soft vs none, per-product shipping allowlists). | -| `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_directive`, `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (v1↔v2 amount field shim — emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlement_override_header`, `dispatch_settlement_by_network`, `extract_payment_signer` (returns `PaymentSigner({address, network})`), `register_x402_schemes_v1_v2`; drop-in x402 helpers: `validate_x402_network_config` (boot-time guard), `verify_x402_request` (parse + validate inbound X-Payment), `process_x402_settle` (verify-then-settle with one call), `classify_x402_settle_result` (maps the tagged settle result to a recommended HTTP status / code / next_steps so merchants get a controlled envelope without coupling to facilitator-specific error text). | -| `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers — `awal x402 details` etc.), `sample_x402_accept_for_network` (USDC sample-accept builder for known CAIP-2 networks), `build_well_known_mpp`, `build_llms_txt` + `llms_txt_identity_section` + `llms_txt_payment_section` (compact + verbose modes), `build_skill_md` (Claude-Skill-compatible `/skill.md` agent-discovery manifest — strictly agent-facing data only, no internal posture), `agentscore_openapi_snippets`, `build_bazaar_discovery_payload`, `NoindexNonDiscoveryMiddleware` (ASGI middleware that emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces — defaults cover `/openapi.json`, `/llms.txt`, `/skill.md`, `/.well-known/{mpp.json,agent-card.json,ucp}`, `/favicon.{png,ico}`; pure helpers `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for non-ASGI frameworks). | -| `agentscore_commerce.challenge` | `build_402_body`, `build_accepted_methods`, `build_identity_metadata`, `build_how_to_pay`, `build_agent_instructions` (auto-emits per-rail `compatible_clients` — smoke-verified CLIs the agent should use; vendor override supported), `build_pricing_block` (cents → dollar-string with optional shipping/tax), `first_encounter_agent_memory` (cross-merchant hint, returns the canonical block or `None` based on a per-merchant first-seen flag), `OrderReceipt` (dataclass for the post-settlement 200 response shape); `respond_402` — drop-in 402 emit that preserves pympp's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `build_validation_error` — structured 4xx body builder (`{error: {code, message}, required_fields?, example_body?, next_steps?, ...extra}`) so vendors compose body shapes by name instead of inlining at every validation site. | +| `agentscore_commerce.identity` (package level) | Re-exports the denial helpers: `denial_reason_status`, `denial_reason_to_body`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `verification_agent_instructions`, `is_fixable_denial`, `FIXABLE_DENIAL_REASONS`. Also re-exports the per-product policy helpers: `PolicyBlock`, `GateResult`, `EnforcementMode`, `IdentityStatus`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed` (for multi-product merchants where each product carries its own compliance config: hard gate vs soft vs none, per-product shipping allowlists). | +| `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_directive`, `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (v1↔v2 amount field shim that emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlement_override_header`, `dispatch_settlement_by_network`, `extract_payment_signer` (returns `PaymentSigner({address, network})`), `register_x402_schemes_v1_v2`; drop-in x402 helpers: `validate_x402_network_config` (boot-time guard), `verify_x402_request` (parse + validate inbound X-Payment), `process_x402_settle` (verify-then-settle with one call), `classify_x402_settle_result` (maps the tagged settle result to a recommended HTTP status / code / next_steps so merchants get a controlled envelope without coupling to facilitator-specific error text). | +| `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers like `awal x402 details`), `sample_x402_accept_for_network` (USDC sample-accept builder for known CAIP-2 networks), `build_well_known_mpp`, `build_llms_txt` + `llms_txt_identity_section` + `llms_txt_payment_section` (compact + verbose modes), `build_skill_md` (Claude-Skill-compatible `/skill.md` agent-discovery manifest; strictly agent-facing data only, no internal posture), `agentscore_openapi_snippets`, `build_bazaar_discovery_payload`, `NoindexNonDiscoveryMiddleware` (ASGI middleware that emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces; defaults cover `/openapi.json`, `/llms.txt`, `/skill.md`, `/.well-known/{mpp.json,agent-card.json,ucp,jwks.json}`, `/favicon.{png,ico}`; pure helpers `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for non-ASGI frameworks). | +| `agentscore_commerce.challenge` | `build_402_body`, `build_accepted_methods`, `build_identity_metadata`, `build_how_to_pay`, `build_agent_instructions` (auto-emits per-rail `compatible_clients`: smoke-verified CLIs the agent should use; vendor override supported), `build_pricing_block` (cents to dollar-string with optional shipping/tax), `first_encounter_agent_memory` (cross-merchant hint, returns the canonical block or `None` based on a per-merchant first-seen flag), `OrderReceipt` (dataclass for the post-settlement 200 response shape); `respond_402`, a drop-in 402 emit that preserves pympp's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `build_validation_error`: structured 4xx body builder (`{error: {code, message}, required_fields?, example_body?, next_steps?, ...extra}`) so vendors compose body shapes by name instead of inlining at every validation site. | | `agentscore_commerce.stripe_multichain` | `create_multichain_payment_intent`, `get_deposit_address`, `simulate_crypto_deposit`; `create_pi_cache` (TTL'd PI / deposit-address cache, Redis-backed when `redis_url` set, in-memory otherwise), `simulate_deposit_if_test_mode` (gates on `sk_test_` and looks up the PI for you), `STRIPE_TEST_TX_HASH_SUCCESS` / `STRIPE_TEST_TX_HASH_FAILED` constants. Peer dep on `stripe`. | -| `agentscore_commerce.api` | Everything from `agentscore-py` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `is_agentscore_test_address`. **Don't add `agentscore-py` as a separate dep** — the two can drift versions and cause subtle type mismatches. | +| `agentscore_commerce.api` | Everything from `agentscore-py` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `is_agentscore_test_address`. **Don't add `agentscore-py` as a separate dep**: the two can drift versions and cause subtle type mismatches. | ## Quick start (FastAPI) @@ -52,7 +52,7 @@ _gate = AgentScoreGate( ) -# Run the gate CONDITIONALLY — only when a payment credential is already attached. +# Run the gate CONDITIONALLY: only when a payment credential is already attached. # Anonymous discovery (no payment header) flows through to the handler so any spec- # compliant x402 wallet can read the 402 challenge with rails + pricing without first # proving identity. Identity is verified at settle time on the retry leg. @@ -99,7 +99,7 @@ directives = [ ] www_auth = www_authenticate_header(directives) -# Recover the on-chain signer (EVM) from an x402 header — returns PaymentSigner | None +# Recover the on-chain signer (EVM) from an x402 header. Returns PaymentSigner | None. signer = extract_payment_signer(request.headers.get("x-payment")) if signer: print(signer.address, signer.network) # ('0x...', 'evm') @@ -181,28 +181,95 @@ headers = build_payment_headers(BuildPaymentHeadersInput( ```python from agentscore_commerce.identity import ( - UCPService, + UCPServiceBinding, UCPSigningKey, - UCPPaymentHandler, + UCPPaymentHandlerBinding, A2AAgentCardCapabilities, build_a2a_agent_card, build_ucp_profile, + ucp_a2a_extension, ) -# Google A2A v1.0 Signed Agent Card — publish at /.well-known/agent-card.json -card = build_a2a_agent_card(name="My Service", url=base_url, capabilities=A2AAgentCardCapabilities(...), data=assess_result) - -# Google Universal Commerce Protocol — publish at /.well-known/ucp +# Google A2A v1.0 Signed Agent Card. Publish at /.well-known/agent-card.json. +# Per UCP §A2A binding the card MUST declare the canonical UCP extension URI; +# pass `ucp_a2a_extension()` with empty capabilities until you bind formal UCP +# capabilities (dev.ucp.shopping.checkout, etc.). +card = build_a2a_agent_card(name="My Service", url=base_url, capabilities=A2AAgentCardCapabilities(...), extensions=[ucp_a2a_extension()], data=assess_result) + +# Google Universal Commerce Protocol. Publish at /.well-known/ucp. +# Output shape: {"ucp": {"version", "services", "capabilities", +# "payment_handlers", "name?", "supported_versions?"}, "signing_keys": [...]} +# — services / capabilities / payment_handlers are MAPS keyed by reverse-DNS +# service / capability / handler name. Verified against the live Pura Vida +# reference at puravidabracelets.com/.well-known/ucp. profile = build_ucp_profile( name="My Service", - services=[UCPService(type="rest", url=base_url)], - payment_handlers=[UCPPaymentHandler(name="tempo", config={"recipient": TEMPO_ADDR})], + services={ + "dev.ucp.shopping": [ + UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/2026-04-08/specification/overview", + transport="mcp", + endpoint=f"{base_url}/api/ucp/mcp", + schema="https://ucp.dev/services/shopping/openrpc.json", + ), + ], + }, + payment_handlers={ + "sh.agentscore.payment.tempo": [ + UCPPaymentHandlerBinding( + id="tempo", + version="2026-04-08", + spec="https://agentscore.sh/specification/payment-handlers/tempo", + schema="https://agentscore.sh/schemas/payment-handlers/tempo.json", + config={"recipient": TEMPO_ADDR}, + ), + ], + }, signing_keys=[UCPSigningKey(kid="me", kty="EC", alg="ES256")], data=assess_result, ) ``` -ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol with no identity-publishing surface — ACP merchants integrate via the existing `build_402_body` + `build_payment_headers` + Stripe SPT rail. +UCP §6 doesn't mandate profile-body JWS signing — Pura Vida and other Shopify-backed UCP merchants ship unsigned. AgentScore's `agentscore-profile+jws` is a vendor extension for trust-mode verifiers (Visa AP2 pilots, regulated-commerce verifiers) that opt into auditable profiles. Sign + verify via the optional `joserfc` extra (tested against joserfc v1.x; pin `joserfc>=1.0.0,<2`): + +```bash +pip install agentscore-commerce[ucp] +``` + +```python +from agentscore_commerce.identity import ( + UCPSigningKey, + UCPVerificationError, + build_jwks_response, + build_ucp_profile, + generate_ucp_signing_key, + sign_ucp_profile, + verify_ucp_profile, +) + +key = generate_ucp_signing_key(kid="merchant-2026-05") +profile = build_ucp_profile( + name="My Service", + services={...}, + payment_handlers={...}, + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], +) +signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=key.public_jwk["kid"], alg="EdDSA") +jwks = build_jwks_response([key.public_jwk]) +``` + +`verify_ucp_profile` enforces the JWS protected header `typ='agentscore-profile+jws'` (vendor-namespaced; UCP §6 does not define a profile-as-JWS typ), restricts `alg` to `EdDSA`/`ES256`, requires a `kid`, rejects duplicate kids in the JWKS, and compares the canonical body bytes against the JWS payload to catch swap-after-sign tampering. Failures raise `UCPVerificationError` (a `ValueError` subclass) with a discriminated `code` attribute (`no_signature`/`missing_kid`/`kid_not_found`/`duplicate_kid`/`unsupported_alg`/`wrong_typ`/`signature_invalid`/`body_mismatch`/`malformed_jws`/`malformed_jwks`/`unusable_key`/`unrecognized_critical_header`). + +`sign_ucp_profile` rejects profiles containing `float` values and `int` values whose magnitude exceeds `Number.MAX_SAFE_INTEGER` (2^53 - 1): cross-language float canonicalization is not stable, and Python's arbitrary-width ints lose precision when JS verifiers reparse the canonical body. Use decimal strings (e.g. `"9.99"`) for monetary or fractional fields and for any integer that may exceed the safe range. + +**Persisting the private JWK.** Mint once via `generate_ucp_signing_key()`, serialize via `key.private_key.as_dict(private=True)`, store in your secret manager. On each container start, read the secret, `OKPKey.import_key(jwk_dict)` (or `ECKey.import_key` for ES256) to re-hydrate. Remote-signer flows (KMS-backed asymmetric keys) require subclassing the joserfc Key to delegate the sign hook; `OKPKey`/`ECKey` themselves only carry local key material. + +**Key rotation.** Mint a new key with a new `kid`, add the public JWK to your JWKS endpoint alongside the old one, then sign new profiles with the new key. Set `Cache-Control: public, max-age=300` on `/.well-known/jwks.json` and wait at least that long after publishing the new key before removing the old JWK. + +**Inline JWK in the profile vs separate JWKS endpoint.** UCP §6 mandates the separate `/.well-known/jwks.json` endpoint as the canonical trust source. The profile's `signing_keys[]` is informational; verifiers MUST resolve the kid against the JWKS to prevent a swap-after-sign attack. + +ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol with no identity-publishing surface; ACP merchants integrate via the existing `build_402_body` + `build_payment_headers` + Stripe SPT rail. ## Stripe multichain @@ -230,15 +297,15 @@ result = create_multichain_payment_intent(CreateMultichainPaymentIntentInput( base_address = get_deposit_address(result, "base") solana_address = get_deposit_address(result, "solana") -# PI / deposit-address cache. Redis-backed when REDIS_URL is set, in-memory otherwise — -# multi-instance deployments need Redis so a deposit lands on whichever instance settles it. +# PI / deposit-address cache. Redis-backed when REDIS_URL is set, in-memory otherwise. +# Multi-instance deployments need Redis so a deposit lands on whichever instance settles it. pi_cache = create_pi_cache(PiCacheOptions(redis_url=os.environ.get("REDIS_URL"))) for addr in result.deposit_addresses.values(): await pi_cache.cache_address(addr) pi_cache.cache_payment_intent(addr, result.payment_intent_id) pi_cache.cache_network_addresses(result.payment_intent_id, result.deposit_addresses) -# Testnet helper — gates on sk_test_ and looks up the PI for you. No-op on live keys. +# Testnet helper. Gates on sk_test_ and looks up the PI for you. No-op on live keys. await simulate_deposit_if_test_mode(SimulateDepositIfTestModeInput( get_payment_intent_id=pi_cache.get_payment_intent_id, deposit_address=base_address, @@ -278,12 +345,12 @@ from agentscore_commerce.payment import ( verify_x402_request, ) -# Boot-time guard — raises if a configured network isn't supported. +# Boot-time guard. Raises if a configured network isn't supported. validate_x402_network_config(ValidateX402NetworkConfigInput(base_network=X402_BASE)) @app.post("/purchase") async def purchase(request: Request): - # Path A — agent presented an x402 X-Payment header + # Path A: agent presented an x402 X-Payment header if request.headers.get("payment-signature") or request.headers.get("x-payment"): verified = await verify_x402_request(VerifyX402RequestInput( headers=dict(request.headers), @@ -311,7 +378,7 @@ async def purchase(request: Request): headers = {"payment-response": settle.payment_response_header} if settle.payment_response_header else {} return JSONResponse({"ok": True}, headers=headers) - # Path B — cold call (or Authorization: Payment for pympp). After pympp.compose() returns 402, + # Path B: cold call (or Authorization: Payment for pympp). After pympp.compose() returns 402, # respond_402 PRESERVES pympp's WWW-Authenticate and ADDS x402's PAYMENT-REQUIRED. result = respond_402(Respond402Input( mppx_challenge_headers=pympp_challenge_headers, @@ -336,14 +403,14 @@ gate = AgentScoreGate(api_key=os.environ["AGENTSCORE_API_KEY"], fail_open=True) async def purchase(request: Request): state = get_gate_degraded_state(request) if state["degraded"]: - # Compliance was NOT enforced this request — log/alert/refund-async/etc. + # Compliance was NOT enforced this request: log/alert/refund-async/etc. logger.warning("gate degraded: %s", state["infra_reason"]) # ...rest of handler ``` -When `fail_open=True` AND the failure is infra-shape, the gate state carries `degraded=True` + `infra_reason="quota_exceeded" | "api_error" | "network_timeout"` so merchants can log/alert without parsing console output. **Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of `fail_open`** — `fail_open` only covers "AgentScore couldn't tell us," never "AgentScore said no." +When `fail_open=True` AND the failure is infra-shape, the gate state carries `degraded=True` + `infra_reason="quota_exceeded" | "api_error" | "network_timeout"` so merchants can log/alert without parsing console output. **Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of `fail_open`**; `fail_open` only covers "AgentScore couldn't tell us," never "AgentScore said no." -For regulated commerce (alcohol, age-gated, sanctioned-jurisdiction-relevant) keep the default `fail_open=False` — outage is the correct posture; bypassing compliance on infra failure is a compliance gap. For low-stakes commerce or high-uptime SLAs, opt in and use the `degraded` flag as the audit trail. +For regulated commerce (alcohol, age-gated, sanctioned-jurisdiction-relevant) keep the default `fail_open=False`; outage is the correct posture, and bypassing compliance on infra failure is a compliance gap. For low-stakes commerce or high-uptime SLAs, opt in and use the `degraded` flag as the audit trail. The `get_gate_degraded_state` helper is exported by every framework adapter (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI middleware) and reads from the framework-appropriate request state. The signature takes a request argument everywhere except Flask, which reads from `g` and takes no arguments. @@ -353,7 +420,7 @@ The [examples/](./examples) directory has 7 runnable single-file FastAPI apps co ## Stability -`agentscore-commerce@1.0.0` ships with the full merchant SDK surface stable. Helpers are protocol translations + configurable opinions — most evolution is additive (new optional params, new helpers, new networks/rails). Major bumps are reserved for genuine protocol-mapping bugs. +`agentscore-commerce@1.4.0` ships with the full merchant SDK surface stable. Helpers are protocol translations + configurable opinions; most evolution is additive (new optional params, new helpers, new networks/rails). Major bumps are reserved for genuine protocol-mapping bugs. ## Documentation diff --git a/agentscore_commerce/discovery/robots_tag.py b/agentscore_commerce/discovery/robots_tag.py index 96b1857..cdf0763 100644 --- a/agentscore_commerce/discovery/robots_tag.py +++ b/agentscore_commerce/discovery/robots_tag.py @@ -27,6 +27,7 @@ "/.well-known/x402", "/.well-known/agent-card.json", "/.well-known/ucp", + "/.well-known/jwks.json", "/favicon.png", "/favicon.ico", } diff --git a/agentscore_commerce/identity/__init__.py b/agentscore_commerce/identity/__init__.py index b4f22fb..fbd1437 100644 --- a/agentscore_commerce/identity/__init__.py +++ b/agentscore_commerce/identity/__init__.py @@ -12,10 +12,13 @@ ) from agentscore_commerce.identity._response import denial_reason_to_body from agentscore_commerce.identity.a2a import ( + UCP_A2A_EXTENSION_URI, A2AAgentCard, A2AAgentCardCapabilities, + A2AAgentCardExtension, A2AAgentCardIdentity, build_a2a_agent_card, + ucp_a2a_extension, ) from agentscore_commerce.identity.client import GateClient from agentscore_commerce.identity.policy import ( @@ -47,13 +50,22 @@ ) from agentscore_commerce.identity.ucp import ( AGENTSCORE_UCP_CAPABILITY, - UCPCapability, - UCPPaymentHandler, + UCPCapabilityBinding, + UCPPaymentHandlerBinding, UCPProfile, - UCPService, + UCPProfileBody, + UCPServiceBinding, UCPSigningKey, build_ucp_profile, ) +from agentscore_commerce.identity.ucp_jwks import ( + GeneratedUCPKey, + UCPVerificationError, + build_jwks_response, + generate_ucp_signing_key, + sign_ucp_profile, + verify_ucp_profile, +) # ASGI middleware is the default import (re-exported as CreateSessionOnMissing too). @@ -79,8 +91,10 @@ def _load_asgi_middleware() -> tuple[Any, Any]: __all__ = [ "AGENTSCORE_UCP_CAPABILITY", "FIXABLE_DENIAL_REASONS", + "UCP_A2A_EXTENSION_URI", "A2AAgentCard", "A2AAgentCardCapabilities", + "A2AAgentCardExtension", "A2AAgentCardIdentity", "Activity", "AgentIdentity", @@ -94,31 +108,39 @@ def _load_asgi_middleware() -> tuple[Any, Any]: "EnforcementMode", "GateClient", "GateResult", + "GeneratedUCPKey", "Grade", "Identity", "IdentityStatus", "OperatorVerification", "PolicyBlock", "ScoreDetail", - "UCPCapability", - "UCPPaymentHandler", + "UCPCapabilityBinding", + "UCPPaymentHandlerBinding", "UCPProfile", - "UCPService", + "UCPProfileBody", + "UCPServiceBinding", "UCPSigningKey", + "UCPVerificationError", "VerifyWalletSignerMatchOptions", "VerifyWalletSignerResult", "build_a2a_agent_card", "build_agent_memory_hint", "build_contact_support_next_steps", "build_gate_from_policy", + "build_jwks_response", "build_signer_mismatch_body", "build_ucp_profile", "denial_reason_status", "denial_reason_to_body", "extract_x402_signer", + "generate_ucp_signing_key", "is_fixable_denial", "run_gate_with_enforcement", "shipping_country_allowed", "shipping_state_allowed", + "sign_ucp_profile", + "ucp_a2a_extension", "verification_agent_instructions", + "verify_ucp_profile", ] diff --git a/agentscore_commerce/identity/a2a.py b/agentscore_commerce/identity/a2a.py index dbdeecf..bb33783 100644 --- a/agentscore_commerce/identity/a2a.py +++ b/agentscore_commerce/identity/a2a.py @@ -23,6 +23,47 @@ _PROTOCOL_VERSION = "1.0" _CARD_VERSION = 1 +UCP_A2A_EXTENSION_URI = "https://ucp.dev/2026-04-08/specification/reference" +"""Canonical UCP A2A extension URI — verifiers look for this exact URI in +``extensions[]`` to detect UCP support on the agent card.""" + + +@dataclass +class A2AAgentCardExtension: + """Per A2A v1.0: an entry in the card's top-level ``extensions`` array. + + UCP support is declared this way (UCP §A2A binding requires + ``https://ucp.dev/2026-04-08/specification/reference``). + """ + + uri: str + params: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = {"uri": self.uri} + if self.params: + out["params"] = self.params + return out + + +def ucp_a2a_extension( + capabilities: dict[str, list[dict[str, str]]] | None = None, +) -> A2AAgentCardExtension: + """Build the canonical UCP entry for an A2A agent card's ``extensions[]`` array. + + Per UCP §A2A binding: "Businesses supporting UCP must advertise the extension + and any optional capabilities in their A2A Agent Card to allow platforms to + activate the extension." Pass the ``capabilities`` map keyed by reverse-DNS + service/capability name (e.g. ``dev.ucp.shopping.checkout``), each value a + list of ``{"version": "..."}`` records. Pass ``None`` (or an empty dict) when + you serve UCP at the discovery layer but have no formal capability bindings + yet. + """ + return A2AAgentCardExtension( + uri=UCP_A2A_EXTENSION_URI, + params={"capabilities": capabilities or {}}, + ) + @dataclass class A2AAgentCardCapabilities: @@ -79,6 +120,7 @@ class A2AAgentCard: description: str | None = None url: str | None = None capabilities: A2AAgentCardCapabilities | None = None + extensions: list[A2AAgentCardExtension] = field(default_factory=list) extras: dict[str, Any] = field(default_factory=dict) protocol_version: str = _PROTOCOL_VERSION card_version: int = _CARD_VERSION @@ -96,6 +138,8 @@ def to_dict(self) -> dict[str, Any]: out["url"] = self.url if self.capabilities is not None: out["capabilities"] = self.capabilities.to_dict() + if self.extensions: + out["extensions"] = [e.to_dict() for e in self.extensions] if self.extras: out.update(self.extras) return out @@ -106,6 +150,7 @@ def build_a2a_agent_card( description: str | None = None, url: str | None = None, capabilities: A2AAgentCardCapabilities | None = None, + extensions: list[A2AAgentCardExtension] | None = None, data: AssessResult | None = None, issuer: str = "https://agentscore.sh", verify_url: str | None = None, @@ -166,13 +211,17 @@ def build_a2a_agent_card( description=description, url=url, capabilities=capabilities, + extensions=extensions or [], extras=extras or {}, ) __all__ = [ + "UCP_A2A_EXTENSION_URI", "A2AAgentCard", "A2AAgentCardCapabilities", + "A2AAgentCardExtension", "A2AAgentCardIdentity", "build_a2a_agent_card", + "ucp_a2a_extension", ] diff --git a/agentscore_commerce/identity/client.py b/agentscore_commerce/identity/client.py index 7962ce6..280ca2b 100644 --- a/agentscore_commerce/identity/client.py +++ b/agentscore_commerce/identity/client.py @@ -218,6 +218,9 @@ def _project(self, data: dict[str, Any]) -> AssessResult: else None ) + av_data = data.get("account_verification") + account_verification = av_data if isinstance(av_data, dict) else None + # SDK populates `quota` on the AssessResponse from X-Quota-* headers. Surface up # to adapters so merchants can monitor approach-to-cap proactively. quota_raw = data.get("quota") @@ -237,6 +240,7 @@ def _project(self, data: dict[str, Any]) -> AssessResult: reasons=reasons, identity_method=data.get("identity_method"), operator_verification=operator_verification, + account_verification=account_verification, resolved_operator=data.get("resolved_operator"), verify_url=data.get("verify_url"), policy_result=data.get("policy_result"), diff --git a/agentscore_commerce/identity/types.py b/agentscore_commerce/identity/types.py index 6b3174e..79d3705 100644 --- a/agentscore_commerce/identity/types.py +++ b/agentscore_commerce/identity/types.py @@ -308,6 +308,11 @@ class AssessResult: reasons: list[str] = field(default_factory=list) identity_method: str | None = None operator_verification: OperatorVerification | None = None + # Account-level verification block (KYC level, age bracket, jurisdiction, + # sanctions verdict). Mirrors node-commerce's typed AgentScoreData.account_verification + # field so a hand-constructed AssessResult emits the same UCP claims in both + # languages without a raw-dict round trip. + account_verification: dict[str, Any] | None = None resolved_operator: str | None = None verify_url: str | None = None policy_result: PolicyResult | None = None diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 52d08b6..d63f765 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -1,45 +1,48 @@ """UCP (Universal Commerce Protocol) profile builder. -Compose the JSON payload published at ``/.well-known/ucp`` per the UCP spec, with -AgentScore identity claims attached as a capability. Returned object is the unsigned -profile body — the merchant signs it (or wraps it in their JWKS-backed envelope) -before publishing. +Compose the JSON payload published at ``/.well-known/ucp`` per the UCP spec. Output +shape matches the spec example: top-level ``{"ucp": {...}, "signing_keys": [...]}`` +envelope, with ``services`` / ``capabilities`` / ``payment_handlers`` as MAPS keyed by +reverse-DNS name. Verified against the live production reference at +``https://puravidabracelets.com/.well-known/ucp`` (Shopify's UCP integration). -Why publish: UCP is the Google-led cross-vendor standard (announced Jan 2026 with -broad ecosystem support). Every UCP-aware platform discovers a merchant via -``/.well-known/ucp``, so shipping this profile means AgentScore-gated merchants are -discoverable through the same surface every other UCP merchant uses. +AgentScore identity claims layer over UCP via the ``sh.agentscore.identity`` capability +(vendor-namespaced; UCP doesn't define KYC/sanctions/age/jurisdiction natively). -Spec reference: https://ucp.dev/ +Pass the unsigned profile through :func:`sign_ucp_profile` to attach the +``agentscore-profile+jws`` signature for trust-mode verifiers (vendor extension; UCP +itself doesn't mandate profile-body signing). -UCP profiles do NOT carry KYC / sanctions / age / jurisdiction claims natively — -identity in the UCP spec is "who signed this" (JWKS-backed). AgentScore claims layer -over UCP via a custom capability so consumers who care about verified-buyer identity -can read them; consumers who don't care just see a normal UCP profile. +Spec reference: https://ucp.dev/ """ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal, cast if TYPE_CHECKING: from agentscore_commerce.identity.types import AssessResult _DEFAULT_VERSION = "2026-04-17" -_SPEC_URL = "https://ucp.dev/" -AGENTSCORE_UCP_CAPABILITY = "agentscore-identity" + +# Reverse-DNS namespacing per UCP convention. The bare ``agentscore-identity`` form +# fails the spec regex; vendor-namespacing under the ``sh.agentscore`` authority is +# honest about the capability being our extension, not a UCP-canonical slot. +AGENTSCORE_UCP_CAPABILITY = "sh.agentscore.identity" """Capability name AgentScore registers in the UCP profile. Consumers filter on this to find verified-buyer claims attached to the profile.""" _AGENTSCORE_CAPABILITY_VERSION = "1" +_AGENTSCORE_DEFAULT_SPEC_URL = "https://agentscore.sh/specification/identity" +_AGENTSCORE_DEFAULT_SCHEMA_URL = "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json" @dataclass class UCPSigningKey: """JWK entry for the profile's ``signing_keys`` array. - Pass through the public key material verbatim — UCP requires JWKS-format keys. + Pass through public key material verbatim; UCP requires JWKS-format keys. """ kid: str @@ -50,6 +53,8 @@ class UCPSigningKey: extras: dict[str, Any] = field(default_factory=dict) """Additional JWK fields (x, y, n, e, etc.) merged into the serialized output.""" + _RESERVED = frozenset({"kid", "kty", "alg", "use", "crv"}) + def to_dict(self) -> dict[str, Any]: out: dict[str, Any] = {"kid": self.kid, "kty": self.kty} if self.alg is not None: @@ -58,59 +63,198 @@ def to_dict(self) -> dict[str, Any]: out["use"] = self.use if self.crv is not None: out["crv"] = self.crv - out.update(self.extras) + for k, v in self.extras.items(): + if k in self._RESERVED: + msg = f"UCPSigningKey.extras key {k!r} collides with a reserved field; rejected." + raise ValueError(msg) + out[k] = v return out + @classmethod + def from_jwk(cls, jwk: dict[str, Any]) -> UCPSigningKey: + """Construct a UCPSigningKey from a public JWK dict. + + Routes the JWK's known fields (kid/kty/alg/use/crv) onto the dataclass and + captures any other fields (x/y/n/e/etc.) into ``extras``. Use this when + publishing the output of :func:`generate_ucp_signing_key` directly. + """ + if not isinstance(jwk, dict): + msg = f"UCPSigningKey.from_jwk expected a dict; got {type(jwk).__name__}." + raise ValueError(msg) + if not isinstance(jwk.get("kid"), str) or not jwk["kid"]: + msg = "UCPSigningKey.from_jwk: JWK missing required field `kid` (or non-string/empty)." + raise ValueError(msg) + if not isinstance(jwk.get("kty"), str) or not jwk["kty"]: + msg = "UCPSigningKey.from_jwk: JWK missing required field `kty` (or non-string/empty)." + raise ValueError(msg) + if jwk["kty"] not in {"OKP", "EC", "RSA"}: + msg = ( + f"UCPSigningKey.from_jwk: kty={jwk['kty']!r} is not a supported " + "asymmetric key type (expected OKP, EC, or RSA). Symmetric `oct` " + "keys are rejected because they cannot publicly verify a JWS in " + "the trust-mode UCP flow." + ) + raise ValueError(msg) + known = {"kid", "kty", "alg", "use", "crv"} + return cls( + kid=jwk["kid"], + kty=jwk["kty"], + alg=jwk.get("alg"), + use=jwk.get("use"), + crv=jwk.get("crv"), + extras={k: v for k, v in jwk.items() if k not in known}, + ) + @dataclass -class UCPService: - """Transport binding entry.""" +class UCPServiceBinding: + """Transport binding entry — keyed under a service name (e.g., ``dev.ucp.shopping``).""" - type: str - url: str | None = None - version: str | None = None + version: str + spec: str + transport: Literal["rest", "mcp", "a2a", "embedded"] + endpoint: str | None = None + schema: str | None = None + id: str | None = None + config: dict[str, Any] | None = None extras: dict[str, Any] = field(default_factory=dict) + _RESERVED = frozenset({"version", "spec", "transport", "endpoint", "schema", "id", "config"}) + def to_dict(self) -> dict[str, Any]: - out: dict[str, Any] = {"type": self.type} - if self.url is not None: - out["url"] = self.url - if self.version is not None: - out["version"] = self.version - out.update(self.extras) + out: dict[str, Any] = { + "version": self.version, + "spec": self.spec, + "transport": self.transport, + } + if self.endpoint is not None: + out["endpoint"] = self.endpoint + if self.schema is not None: + out["schema"] = self.schema + if self.id is not None: + out["id"] = self.id + if self.config: + out["config"] = self.config + for k, v in self.extras.items(): + if k in self._RESERVED: + msg = f"UCPServiceBinding.extras key {k!r} collides with a reserved field; rejected." + raise ValueError(msg) + out[k] = v return out @dataclass -class UCPCapability: - """Capability entry — name + schema URL + version + claims.""" - - name: str - schema: str | None = None - version: str | None = None +class UCPCapabilityBinding: + """Capability binding entry — keyed under a capability name (e.g., ``dev.ucp.shopping.checkout``).""" + + version: str + spec: str + schema: str + id: str | None = None + config: dict[str, Any] | None = None + extends: str | list[str] | None = None + requires: dict[str, Any] | None = None extras: dict[str, Any] = field(default_factory=dict) + _RESERVED = frozenset({"version", "spec", "schema", "id", "config", "extends", "requires"}) + def to_dict(self) -> dict[str, Any]: - out: dict[str, Any] = {"name": self.name} - if self.schema is not None: - out["schema"] = self.schema - if self.version is not None: - out["version"] = self.version - out.update(self.extras) + out: dict[str, Any] = { + "version": self.version, + "spec": self.spec, + "schema": self.schema, + } + if self.id is not None: + out["id"] = self.id + if self.config: + out["config"] = self.config + if self.extends is not None: + out["extends"] = self.extends + if self.requires is not None: + out["requires"] = self.requires + for k, v in self.extras.items(): + if k in self._RESERVED: + msg = f"UCPCapabilityBinding.extras key {k!r} collides with a reserved field; rejected." + raise ValueError(msg) + out[k] = v return out @dataclass -class UCPPaymentHandler: - """Payment handler entry — name + config.""" +class UCPPaymentHandlerBinding: + """Payment handler binding entry — keyed under a handler reverse-DNS name (e.g., ``com.google.pay``).""" + + id: str + version: str + spec: str + schema: str + available_instruments: list[dict[str, Any]] | None = None + config: dict[str, Any] | None = None + extras: dict[str, Any] = field(default_factory=dict) - name: str - config: dict[str, Any] = field(default_factory=dict) + _RESERVED = frozenset({"id", "version", "spec", "schema", "available_instruments", "config"}) def to_dict(self) -> dict[str, Any]: - out: dict[str, Any] = {"name": self.name} + out: dict[str, Any] = { + "id": self.id, + "version": self.version, + "spec": self.spec, + "schema": self.schema, + } + if self.available_instruments is not None: + out["available_instruments"] = self.available_instruments if self.config: out["config"] = self.config + for k, v in self.extras.items(): + if k in self._RESERVED: + msg = f"UCPPaymentHandlerBinding.extras key {k!r} collides with a reserved field; rejected." + raise ValueError(msg) + out[k] = v + return out + + +@dataclass +class UCPProfileBody: + """UCP body — nested under the ``ucp`` key of the published profile.""" + + version: str = _DEFAULT_VERSION + services: dict[str, list[UCPServiceBinding]] = field(default_factory=dict) + capabilities: dict[str, list[UCPCapabilityBinding]] = field(default_factory=dict) + payment_handlers: dict[str, list[UCPPaymentHandlerBinding]] = field(default_factory=dict) + name: str | None = None + supported_versions: dict[str, str] | None = None + extras: dict[str, Any] = field(default_factory=dict) + + _RESERVED = frozenset( + { + "version", + "name", + "services", + "capabilities", + "payment_handlers", + "supported_versions", + "__proto__", + "constructor", + "prototype", + }, + ) + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = { + "version": self.version, + "services": {k: [s.to_dict() for s in bindings] for k, bindings in self.services.items()}, + "capabilities": {k: [c.to_dict() for c in bindings] for k, bindings in self.capabilities.items()}, + "payment_handlers": {k: [h.to_dict() for h in bindings] for k, bindings in self.payment_handlers.items()}, + } + if self.name is not None: + out["name"] = self.name + if self.supported_versions is not None: + out["supported_versions"] = self.supported_versions + for k, v in self.extras.items(): + if k in self._RESERVED: + msg = f"UCPProfileBody.extras key {k!r} collides with a reserved `ucp` field; rejected." + raise ValueError(msg) + out[k] = v return out @@ -118,58 +262,64 @@ def to_dict(self) -> dict[str, Any]: class UCPProfile: """UCP profile body for ``/.well-known/ucp``. - Use :meth:`to_dict` to serialize. Sign + envelope with your JWKS-backed signing - flow before publishing. + Top-level shape: ``{"ucp": {...}, "signing_keys": [...], "signature?": "..."}``. + Use :meth:`to_dict` to serialize. Pass through :func:`sign_ucp_profile` to attach + the JWS signature. """ - services: list[UCPService] = field(default_factory=list) - capabilities: list[UCPCapability] = field(default_factory=list) - payment_handlers: list[UCPPaymentHandler] = field(default_factory=list) + ucp: UCPProfileBody = field(default_factory=UCPProfileBody) signing_keys: list[UCPSigningKey] = field(default_factory=list) - name: str | None = None - version: str = _DEFAULT_VERSION - spec: str = _SPEC_URL extras: dict[str, Any] = field(default_factory=dict) + _RESERVED = frozenset( + {"ucp", "signing_keys", "signature", "__proto__", "constructor", "prototype"}, + ) + def to_dict(self) -> dict[str, Any]: out: dict[str, Any] = { - "version": self.version, - "spec": self.spec, - "services": [s.to_dict() for s in self.services], - "capabilities": [c.to_dict() for c in self.capabilities], - "payment_handlers": [h.to_dict() for h in self.payment_handlers], + "ucp": self.ucp.to_dict(), "signing_keys": [k.to_dict() for k in self.signing_keys], } - if self.name is not None: - out["name"] = self.name - out.update(self.extras) + for k, v in self.extras.items(): + if k in self._RESERVED: + msg = f"UCPProfile.extras key {k!r} collides with a reserved profile field; rejected." + raise ValueError(msg) + out[k] = v return out def build_ucp_profile( - services: list[UCPService], - signing_keys: list[UCPSigningKey], - capabilities: list[UCPCapability] | None = None, - payment_handlers: list[UCPPaymentHandler] | None = None, + services: dict[str, list[UCPServiceBinding]] | None = None, + signing_keys: list[UCPSigningKey] | None = None, + *, + capabilities: dict[str, list[UCPCapabilityBinding]] | None = None, + payment_handlers: dict[str, list[UCPPaymentHandlerBinding]] | None = None, name: str | None = None, version: str = _DEFAULT_VERSION, data: AssessResult | None = None, agentscore_schema_url: str | None = None, + agentscore_spec_url: str | None = None, + supported_versions: dict[str, str] | None = None, + ucp_extras: dict[str, Any] | None = None, extras: dict[str, Any] | None = None, ) -> UCPProfile: """Compose a UCP profile body for ``/.well-known/ucp`` publication. - Merges AgentScore identity claims into ``capabilities`` as an - ``agentscore-identity`` capability when ``data`` carries a resolved operator. - Consumers reading the profile can opt into the AgentScore claims by filtering - on the capability name. + Returns the spec-compliant shape: ``{"ucp": {...}, "signing_keys": [...]}`` + with ``services`` / ``capabilities`` / ``payment_handlers`` as maps keyed by + reverse-DNS name. Pass through :func:`sign_ucp_profile` to attach a JWS + signature for trust-mode verifiers. + + Auto-injects ``sh.agentscore.identity`` as a vendor capability when ``data`` + carries a resolved operator. Verifiers that recognize the AgentScore namespace + can parse the ``claims`` extra; vanilla UCP agents see a normal capability. Example:: from agentscore_commerce.identity.ucp import ( - UCPService, + UCPServiceBinding, UCPSigningKey, - UCPPaymentHandler, + UCPPaymentHandlerBinding, build_ucp_profile, ) @@ -177,65 +327,125 @@ def build_ucp_profile( async def ucp_profile(): result = await client.acheck(identity) return build_ucp_profile( + services={ + "dev.ucp.shopping": [ + UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/2026-04-08/specification/overview", + transport="mcp", + endpoint="https://merchant.example/api/ucp/mcp", + schema="https://ucp.dev/services/shopping/openrpc.json", + ), + ], + }, + signing_keys=[UCPSigningKey.from_jwk(public_jwk)], + payment_handlers={ + "sh.agentscore.payment.tempo": [ + UCPPaymentHandlerBinding( + id="tempo", + version="2026-04-08", + spec="https://agentscore.sh/specification/payment-handlers/tempo", + schema="https://agentscore.sh/schemas/payment-handlers/tempo.json", + config={"recipient": TEMPO_ADDR}, + ), + ], + }, name="Example Merchant", - services=[UCPService(type="rest", url="https://agents.example.com")], - payment_handlers=[ - UCPPaymentHandler(name="tempo", config={"recipient": TEMPO_ADDR}), - UCPPaymentHandler(name="stripe", config={"profile_id": STRIPE_PROFILE_ID}), - ], - signing_keys=[ - UCPSigningKey(kid="merchant-2026-04", kty="EC", alg="ES256", crv="P-256", - extras={"x": "...", "y": "..."}), - ], data=result, ).to_dict() """ - base_capabilities = list(capabilities or []) + services = services if services is not None else {} + signing_keys = signing_keys if signing_keys is not None else [] + + # Deep-copy the capabilities map so we can safely mutate (auto-inject the + # AgentScore identity capability) without altering the caller's input. + base_capabilities: dict[str, list[UCPCapabilityBinding]] = { + k: list(bindings) for k, bindings in (capabilities or {}).items() + } if data is not None and data.resolved_operator: - raw = data.raw or {} - operator_verification = raw.get("operator_verification") if isinstance(raw, dict) else None - account_verification = raw.get("account_verification") if isinstance(raw, dict) else None - if not isinstance(operator_verification, dict): - operator_verification = {} - if not isinstance(account_verification, dict): + # Read typed AssessResult fields first (canonical path). Fall back to + # ``data.raw["operator_verification"]`` / ``data.raw["account_verification"]`` + # only when the typed field is ``None`` (Python-only legacy escape hatch + # for callers who hand-construct ``AssessResult(raw=..., typed=None)``). + # Node has no raw fallback at all. + typed_op = data.operator_verification + operator_verification: dict[str, Any] + if typed_op is None: + raw = data.raw or {} + raw_op = raw.get("operator_verification") if isinstance(raw, dict) else None + operator_verification = raw_op if isinstance(raw_op, dict) else {} + elif isinstance(typed_op, dict): + operator_verification = cast("dict[str, Any]", typed_op) + else: + operator_verification = { + "level": getattr(typed_op, "level", None), + "operator_type": getattr(typed_op, "operator_type", None), + "verified_at": getattr(typed_op, "verified_at", None), + } + + account_verification: dict[str, Any] + if data.account_verification is None: + raw = data.raw or {} + raw_av = raw.get("account_verification") if isinstance(raw, dict) else None + account_verification = raw_av if isinstance(raw_av, dict) else {} + elif isinstance(data.account_verification, dict): + account_verification = data.account_verification + else: account_verification = {} + + # `dict.get(k) or DEFAULT` (not `dict.get(k, DEFAULT)`) coerces both a + # missing key AND a present-but-falsy (None / "") value to the default, + # matching the node sibling's `||` semantics. claims = { "operator_id": data.resolved_operator, "kyc_level": account_verification.get("kyc_level") or operator_verification.get("level") or "none", "sanctions_clear": account_verification.get("sanctions_clear") is True, - "age_bracket": account_verification.get("age_bracket", "unknown"), - "jurisdiction": account_verification.get("jurisdiction", ""), - "verified_at": account_verification.get("verified_at") or operator_verification.get("verified_at"), + "age_bracket": account_verification.get("age_bracket") or "unknown", + "jurisdiction": account_verification.get("jurisdiction") or "", + "verified_at": account_verification.get("verified_at") or operator_verification.get("verified_at") or None, "verify_url": data.verify_url, "issuer": "https://agentscore.sh", } - base_capabilities.append( - UCPCapability( - name=AGENTSCORE_UCP_CAPABILITY, - version=_AGENTSCORE_CAPABILITY_VERSION, - schema=agentscore_schema_url or "https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json", - extras={"claims": claims}, - ), + # Multi-parent extension matching Shopify's `dev.shopify.catalog.storefront` + # and UCP-canonical `dev.ucp.shopping.discount` (extends [checkout, cart]). + # `claims` lives in `extras` so it serializes as a vendor field on the binding. + binding = UCPCapabilityBinding( + version=_AGENTSCORE_CAPABILITY_VERSION, + spec=agentscore_spec_url or _AGENTSCORE_DEFAULT_SPEC_URL, + schema=agentscore_schema_url or _AGENTSCORE_DEFAULT_SCHEMA_URL, + extends=["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"], + extras={"claims": claims}, ) + if AGENTSCORE_UCP_CAPABILITY in base_capabilities: + base_capabilities[AGENTSCORE_UCP_CAPABILITY].append(binding) + else: + base_capabilities[AGENTSCORE_UCP_CAPABILITY] = [binding] - return UCPProfile( + body = UCPProfileBody( + version=version, services=services, capabilities=base_capabilities, - payment_handlers=list(payment_handlers or []), - signing_keys=signing_keys, + payment_handlers=payment_handlers if payment_handlers is not None else {}, name=name, - version=version, + supported_versions=supported_versions, + extras=ucp_extras or {}, + ) + + return UCPProfile( + ucp=body, + signing_keys=signing_keys, extras=extras or {}, ) __all__ = [ "AGENTSCORE_UCP_CAPABILITY", - "UCPCapability", - "UCPPaymentHandler", + "UCPCapabilityBinding", + "UCPPaymentHandlerBinding", "UCPProfile", - "UCPService", + "UCPProfileBody", + "UCPServiceBinding", "UCPSigningKey", "build_ucp_profile", ] diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py new file mode 100644 index 0000000..42d6a1a --- /dev/null +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -0,0 +1,542 @@ +"""UCP profile signing helpers (JWKS + JWS) — Python sibling of node-commerce. + +UCP §6 (https://ucp.dev/latest/specification/signatures/) requires that profiles +published at ``/.well-known/ucp`` carry a JWKS-backed signature for trust-mode clients +(Google AI Mode, Gemini commerce, future ChatGPT app shells). Without a signature, +trust-mode clients reject the profile. + +This module provides: + +* :func:`generate_ucp_signing_key` — generate an Ed25519 (or ES256) keypair +* :func:`sign_ucp_profile` — sign a profile, returning a JWS-attached envelope +* :func:`verify_ucp_profile` — verify a signed profile against a JWKS +* :func:`build_jwks_response` — assemble a JWKS document for ``/.well-known/jwks.json`` + +Implementation rides on ``joserfc`` (optional extra). Install via +``pip install agentscore-commerce[ucp]``. Merchants who don't sign their profile +(development) skip this module entirely; the unsigned :func:`build_ucp_profile` +path still works. + +Cross-language API parity with ``@agent-score/commerce`` Node SDK — same canonical +body, same JWS Compact Serialization, same key-resolution semantics. Profiles +signed by Node verify in Python and vice versa. +""" + +from __future__ import annotations + +import contextlib +import hmac +import json +import warnings +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Literal, cast + +if TYPE_CHECKING: + from collections.abc import Iterator + +_JOSE_INSTALL_HINT = ( + "Install the optional dependency: `pip install agentscore-commerce[ucp]` (or `uv pip install joserfc`)." +) + +_ALLOWED_ALGS = ("EdDSA", "ES256") +# JWS protected header ``typ`` value. Vendor-namespaced because UCP §6 does not define +# a profile-as-JWS typ; the value advertises that this signed envelope follows the +# AgentScore extension semantics rather than a UCP-canonical signing convention. +_PROFILE_TYP = "agentscore-profile+jws" + +_MAX_SAFE_INT = 2**53 - 1 + + +@contextlib.contextmanager +def _suppress_joserfc_eddsa_warning() -> Iterator[None]: + """Suppress joserfc's RFC-9864-deprecation SecurityWarning around JWS sign/verify. + + joserfc emits this on every JWS operation that uses EdDSA, despite EdDSA + being the actively-recommended-by-IETF algorithm for new deployments. The + filter is pinned to the exact message + class + (``joserfc.errors.SecurityWarning``: ``"EdDSA is deprecated via RFC 9864"``) + so any other SecurityWarning still surfaces normally. Key generation does + not emit this warning, so suppression has no effect there. + """ + from joserfc.errors import SecurityWarning # type: ignore[import-not-found] + + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message=r"^EdDSA is deprecated via RFC 9864$", + category=SecurityWarning, + ) + yield + + +class UCPVerificationError(ValueError): + """Discriminated error for UCP signature verification failures. + + Subclasses ``ValueError`` so existing ``except ValueError`` blocks keep working. + Inspect ``code`` to branch on failure mode without parsing the message string + or importing joserfc internals. + """ + + def __init__( + self, + code: Literal[ + "no_signature", + "missing_kid", + "kid_not_found", + "duplicate_kid", + "unsupported_alg", + "wrong_typ", + "signature_invalid", + "body_mismatch", + "malformed_jws", + "malformed_jwks", + "unrecognized_critical_header", + "unusable_key", + ], + message: str, + ) -> None: + super().__init__(message) + self.code = code + + +def _load_joserfc() -> Any: + """Lazy-import joserfc so the optional dep isn't required for non-signing flows.""" + try: + import joserfc # type: ignore[import-not-found] + + return joserfc + except ImportError as exc: + msg = f"UCP signing requires the `joserfc` library, an optional dependency. {_JOSE_INSTALL_HINT}" + raise ImportError(msg) from exc + + +@dataclass +class GeneratedUCPKey: + """Output of :func:`generate_ucp_signing_key`. + + * ``private_key`` is the joserfc Key object — pass to :func:`sign_ucp_profile`. + Never publish. + * ``public_jwk`` is the JWK dict — publish at ``/.well-known/jwks.json`` and + inline in the UCP profile's ``signing_keys[]``. + """ + + private_key: Any + public_jwk: dict[str, Any] + + +def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDSA") -> GeneratedUCPKey: + """Generate an Ed25519 (default) or ES256 keypair for signing UCP profiles. + + The ``private_key`` is a joserfc ``Key`` — store it securely (env var, KMS, secret + manager) and pass to :func:`sign_ucp_profile`. + + The ``public_jwk`` is a dict you publish at ``/.well-known/jwks.json`` and inline + in the UCP profile's ``signing_keys[]`` array. + + Example:: + + from agentscore_commerce.identity.ucp_jwks import generate_ucp_signing_key + + key = generate_ucp_signing_key(kid='merchant-2026-05') + # key.private_key — persist securely + # key.public_jwk — publish at /.well-known/jwks.json + """ + _load_joserfc() + + if alg == "EdDSA": + from joserfc.jwk import OKPKey # type: ignore[import-not-found] + + priv = OKPKey.generate_key(crv="Ed25519", parameters={"kid": kid, "alg": alg, "use": "sig"}) + elif alg == "ES256": + from joserfc.jwk import ECKey # type: ignore[import-not-found] + + priv = ECKey.generate_key(crv="P-256", parameters={"kid": kid, "alg": alg, "use": "sig"}) + else: + msg = f"Unsupported UCP signing algorithm: {alg!r}. Use 'EdDSA' or 'ES256'." + raise ValueError(msg) + + public_jwk = priv.as_dict(private=False) + # Ensure kid/alg/use are present in the exported dict (joserfc preserves params). + public_jwk.setdefault("kid", kid) + public_jwk.setdefault("alg", alg) + public_jwk.setdefault("use", "sig") + + return GeneratedUCPKey(private_key=priv, public_jwk=public_jwk) + + +def _reject_unsafe_numbers(value: Any) -> None: + """Walk ``value`` and raise on anything that won't survive cross-language parity. + + Three failure modes are rejected: + + * Non-integer ``float`` values. Cross-language float canonicalization (RFC 8785 + §3.2.2.3) diverges between Python's ``json.dumps`` and Node's ``JSON.stringify`` + (e.g. ``1.0`` vs ``1``, ``1e-7`` vs ``1e-07``). Use decimal strings (``"9.99"``) + for monetary or fractional fields. + * ``int`` values whose magnitude exceeds ``Number.MAX_SAFE_INTEGER`` (2^53 - 1). + Python ints are arbitrary-width, but JS verifiers parse the canonical body via + ``JSON.parse`` which silently loses precision past 2^53. Use a decimal string + for any integer that may exceed the safe range. + * Strings containing U+2028 (LINE SEPARATOR) or U+2029 (PARAGRAPH SEPARATOR). + Pre-ES2019 V8 (and any environment whose ``JSON.stringify`` still escapes + these codepoints) emits the escaped sequences while + ``json.dumps(ensure_ascii=False)`` emits them raw, so the canonical bytes + would diverge across the Node and Python siblings. Mirror of the rejection + in ``core/api/src/lib/canonicalize.ts``. + + Catching the drift at sign-time prevents silent verifier-side failures in + production. + """ + if isinstance(value, bool): + return # bool subclasses int; allow. + if isinstance(value, float): + msg = ( + f"UCP profile canonicalization rejects float value {value!r}. " + "Use a decimal string (e.g. '9.99') for monetary or fractional fields " + "to preserve cross-language byte-parity." + ) + raise ValueError(msg) + if isinstance(value, int) and abs(value) > _MAX_SAFE_INT: + msg = ( + f"UCP profile canonicalization rejects integer {value} that exceeds " + "Number.MAX_SAFE_INTEGER (2^53 - 1). JS verifiers cannot losslessly " + "parse this; use a decimal string to preserve cross-language byte-parity." + ) + raise ValueError(msg) + if isinstance(value, str): + if "\u2028" in value or "\u2029" in value: + msg = ( + "UCP profile strings containing U+2028 (LINE SEPARATOR) or " + "U+2029 (PARAGRAPH SEPARATOR) are not allowed; cross-language " + "byte parity requires neither be present (Node JSON.stringify " + "on older V8 escapes them; Python json.dumps with " + "ensure_ascii=False does not)." + ) + raise ValueError(msg) + return + # Reject set / frozenset with a typed message (mirrors the node sibling's + # "Set values are not allowed" rejection in stableStringify). Without this, + # an empty set or a set-of-valid-strings falls through `_reject_unsafe_numbers` + # cleanly and surfaces a raw `TypeError` from `json.dumps` later. Sets aren't + # representable in JSON; convert to a sorted list before passing. + if isinstance(value, set | frozenset): + msg = ( + f"{type(value).__name__} values are not allowed in canonicalized JSON. " + "Convert to a sorted list before passing." + ) + raise ValueError(msg) + # Reject bytes / bytearray with a typed message (mirrors the node sibling's + # "typed arrays are not allowed" rejection in stableStringify). Without this, + # raw bytes fall through cleanly and surface a confusing + # `TypeError: Object of type bytes is not JSON serializable` from + # `json.dumps` later. Convert to a base64url string before passing. + if isinstance(value, bytes | bytearray): + msg = ( + f"{type(value).__name__} values are not allowed in canonicalized JSON. " + "Convert to a base64url string before passing." + ) + raise ValueError(msg) + if isinstance(value, dict): + for k, v in value.items(): + _reject_unsafe_numbers(k) + _reject_unsafe_numbers(v) + elif isinstance(value, list | tuple): + for v in value: + _reject_unsafe_numbers(v) + + +def _canonicalize_profile(profile: dict[str, Any]) -> bytes: + """Canonicalize a UCP profile body for signing. + + Removes the ``signature`` field (if present), sorts keys lexicographically at every + nesting level, returns UTF-8 JSON bytes. Cross-language byte-identical with the + Node ``stableStringify`` output. + + Throws ``ValueError`` on float input or oversized int (see + :func:`_reject_unsafe_numbers`). + + UCP §6.2: "the JSON-serialized profile body, with ``signature`` removed and keys + ordered lexicographically at every nesting level." + """ + stripped = {k: v for k, v in profile.items() if k != "signature"} + _reject_unsafe_numbers(stripped) + # ``ensure_ascii=False`` so non-ASCII characters travel as UTF-8 (matches Node's + # JSON.stringify default). ``sort_keys=True`` sorts keys at every level. Compact + # separators avoid whitespace drift. + return json.dumps(stripped, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + + +def sign_ucp_profile( + profile: dict[str, Any], + *, + signing_key: Any, + kid: str, + alg: Literal["EdDSA", "ES256"] = "EdDSA", +) -> dict[str, Any]: + """Sign a UCP profile, returning a new dict with the JWS attached as ``signature``. + + The signature covers the canonicalized profile body (everything except + ``signature`` itself, with keys sorted at every level). Trust-mode UCP verifiers + reconstruct the canonical body, look up the key referenced by the JWS header's + ``kid``, and validate. + + The profile's ``signing_keys[]`` MUST already include a JWK with the matching + ``kid`` — otherwise verifiers can't find the public key. + + Example:: + + profile = build_ucp_profile(..., signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)]) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid='merchant-2026-05') + """ + _load_joserfc() + from joserfc import jws # type: ignore[import-not-found] + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + if not isinstance(kid, str) or not kid: + msg = "sign_ucp_profile: `kid` must be a non-empty string." + raise ValueError(msg) + + # Sign-time kid sanity check: the profile's `signing_keys[]` MUST contain + # a JWK with the matching kid; otherwise verifiers can't resolve the + # public key and the profile is dead-on-arrival. + declared_kids = [ + k.get("kid") if isinstance(k, dict) else getattr(k, "kid", None) for k in profile.get("signing_keys", []) + ] + if kid not in declared_kids: + msg = ( + f"sign_ucp_profile: kid {kid!r} is not present in profile.signing_keys[] " + f"(declared kids: {declared_kids!r}). Verifiers will not find the key." + ) + raise ValueError(msg) + + canonical_body = _canonicalize_profile(profile) + header = {"alg": alg, "kid": kid, "typ": _PROFILE_TYP} + # joserfc treats EdDSA as "not recommended" by default; UCP §6 explicitly accepts + # both EdDSA and ES256, so allow both. + registry = JWSRegistry(algorithms=list(_ALLOWED_ALGS)) + with _suppress_joserfc_eddsa_warning(): + signature = jws.serialize_compact(header, canonical_body, signing_key, registry=registry) + + return {**profile, "signature": signature} + + +def _peek_jws_header(jws_compact: str) -> dict[str, Any]: + """Decode the JWS protected header (first segment) without verifying. + + Used to enforce kid/typ/alg requirements before handing the JWS to joserfc's + deserialize_compact (which would skip these checks for kid-less JWSs). + """ + import base64 + + try: + header_b64 = jws_compact.split(".")[0] + padding = "=" * (-len(header_b64) % 4) + header_bytes = base64.urlsafe_b64decode(header_b64 + padding) + decoded = json.loads(header_bytes) + except (ValueError, IndexError, json.JSONDecodeError) as exc: + raise UCPVerificationError("malformed_jws", f"Could not decode JWS protected header: {exc}") from exc + if not isinstance(decoded, dict): + raise UCPVerificationError( + "malformed_jws", + f"JWS protected header must decode to a JSON object; got {type(decoded).__name__}.", + ) + return decoded + + +def verify_ucp_profile( + signed_profile: dict[str, Any], + jwks: dict[str, Any], +) -> bool: + """Verify a signed UCP profile against a JWKS. + + Returns ``True`` when: + * the JWS protected header carries ``kid`` + ``typ='agentscore-profile+jws'`` + a + registered ``alg`` (EdDSA or ES256), + * the JWKS contains exactly one key with the matching ``kid``, + * the JWS signature validates against that key, + * the signed payload byte-equals the canonical body of the presented profile. + + Raises :class:`UCPVerificationError` (a ``ValueError`` subclass) with a + discriminated ``code`` attribute on every failure mode. + + Example:: + + ok = verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + """ + _load_joserfc() + from joserfc import jws # type: ignore[import-not-found] + from joserfc.jwk import KeySet # type: ignore[import-not-found] + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + if not isinstance(signed_profile, dict): + raise UCPVerificationError( + "no_signature", + f"UCP verifier expected a profile dict; got {type(signed_profile).__name__}.", + ) + + # JWKS shape guard so a malformed argument emits a typed UCPVerificationError + # rather than a confusing kid_not_found / AttributeError. + if not isinstance(jwks, dict) or not isinstance(jwks.get("keys"), list): + raise UCPVerificationError( + "malformed_jwks", + f"UCP verifier expected JWKS shape {{'keys': [...]}}; got {type(jwks).__name__}.", + ) + + sig = signed_profile.get("signature") + if not sig: + raise UCPVerificationError( + "no_signature", + "UCP profile has no `signature` field; expected JWS Compact Serialization.", + ) + if not isinstance(sig, str): + raise UCPVerificationError( + "no_signature", + f"UCP `signature` must be a string; got {type(sig).__name__}.", + ) + + # Pre-deserialize header checks — joserfc's deserialize_compact accepts kid-less + # JWSs (it iterates the KeySet) so we enforce kid/typ/alg ourselves. + header = _peek_jws_header(sig) + if header.get("typ") != _PROFILE_TYP: + raise UCPVerificationError( + "wrong_typ", + f"UCP signature typ must be {_PROFILE_TYP!r}; got {header.get('typ')!r}.", + ) + if header.get("alg") not in _ALLOWED_ALGS: + raise UCPVerificationError( + "unsupported_alg", + f"UCP signing alg must be one of {_ALLOWED_ALGS}; got {header.get('alg')!r}.", + ) + kid = header.get("kid") + if not kid or not isinstance(kid, str): + raise UCPVerificationError("missing_kid", "UCP signature header missing `kid`.") + + # UCP doesn't define any critical headers; any crit advertised is by definition + # unrecognized. Reject before the JWKS kid lookup so a crit-violating JWS with a + # missing/duplicate/unusable kid surfaces crit (not kid_not_found / duplicate_kid / + # unusable_key), matching node-commerce's manual peek order: + # typ -> alg -> kid -> crit -> kid_lookup. Cross-language ordering parity is + # non-obvious because joserfc's deserialize_compact only enforces crit AFTER + # the kid lookup, so we must check it here ourselves. + # Gate on key-presence (not `is not None`) so that JSON `null` falls through to + # the shape check and surfaces typed `malformed_jws`, not joserfc's raw TypeError + # when it tries to iterate `None`. RFC 7515 §4.1.11 requires a non-empty array. + if "crit" in header: + crit = header["crit"] + if not isinstance(crit, list) or len(crit) == 0 or not all(isinstance(c, str) for c in crit): + raise UCPVerificationError( + "malformed_jws", + f"JWS protected header crit must be a non-empty array of strings; got {crit!r}.", + ) + raise UCPVerificationError( + "unrecognized_critical_header", + f"JWS protected header advertises unrecognized crit headers: {crit!r}.", + ) + + keys_list = jwks.get("keys", []) if isinstance(jwks, dict) else [] + matches = [k for k in keys_list if isinstance(k, dict) and k.get("kid") == kid] + if not matches: + raise UCPVerificationError("kid_not_found", f"No JWK in JWKS matching kid={kid!r}.") + if len(matches) > 1: + raise UCPVerificationError( + "duplicate_kid", + f"JWKS contains {len(matches)} keys with kid={kid!r}; expected exactly one.", + ) + matched = matches[0] + # RFC 7517 §4.2: reject keys not intended for signature verification. + # ``use`` and ``alg`` are optional per RFC 7517; an explicit JSON null is + # out-of-spec but treat it as absent (skip-on-null) so a JWK with + # ``"use": null`` matches the Node sibling's ``!= null`` semantics in + # ucp-jwks.ts and the two languages stay symmetric. + matched_use = matched.get("use") + if matched_use is not None and matched_use != "sig": + raise UCPVerificationError( + "unusable_key", + f"JWK with kid={kid!r} has use={matched_use!r}; expected 'sig'.", + ) + # RFC 7517 §4.4: a JWK with declared `alg` constrains its use to that algorithm. + header_alg = header.get("alg") + matched_alg = matched.get("alg") + if matched_alg is not None and matched_alg != header_alg: + raise UCPVerificationError( + "unusable_key", + f"JWK alg {matched_alg!r} does not match JWS header alg {header_alg!r}.", + ) + # joserfc's KeySet.import_key_set runs a stricter dict-key validation that + # rejects ``use: None`` / ``alg: None`` outright. Strip explicit nulls for + # those two fields before handing the JWK off so skip-on-null actually + # propagates to the import step. + matches = [{k: v for k, v in matched.items() if not (k in ("use", "alg") and v is None)}] + + stripped = {k: v for k, v in signed_profile.items() if k != "signature"} + try: + expected_payload = _canonicalize_profile(stripped) + except (ValueError, TypeError) as exc: + raise UCPVerificationError( + "body_mismatch", + f"Failed to canonicalize received profile for verification: {exc}", + ) from exc + + key_set = KeySet.import_key_set(cast("Any", {"keys": matches})) + registry = JWSRegistry(algorithms=list(_ALLOWED_ALGS)) + try: + with _suppress_joserfc_eddsa_warning(): + obj = jws.deserialize_compact(sig, key_set, registry=registry) + except Exception as exc: + # joserfc raises various subclasses. Wrap in our own type so callers + # don't need to import joserfc internals. + from joserfc.errors import ( # type: ignore[import-not-found] + BadSignatureError, + DecodeError, + UnsupportedHeaderError, + ) + + if isinstance(exc, BadSignatureError): + raise UCPVerificationError("signature_invalid", f"UCP signature verification failed: {exc}") from exc + if isinstance(exc, DecodeError): + raise UCPVerificationError("malformed_jws", f"Malformed JWS: {exc}") from exc + # RFC 7515 §4.1.11 / RFC 8725 §3.10: a verifier MUST reject any JWS + # whose `crit` header carries an extension the implementation doesn't + # understand. + if isinstance(exc, UnsupportedHeaderError): + raise UCPVerificationError( + "unrecognized_critical_header", + f"UCP signing rejected unrecognized critical header: {exc}", + ) from exc + raise + + # Compare the bytes that were actually signed against the canonical body of the + # profile we received. ``deserialize_compact`` validates the JWS against the bytes + # embedded in the JWS payload segment — but the profile body could have been + # swapped after signing while the JWS stayed unchanged. + if not hmac.compare_digest(obj.payload, expected_payload): + raise UCPVerificationError( + "body_mismatch", + "UCP profile body does not match the signed payload (tampered or non-canonical).", + ) + + return True + + +def build_jwks_response(keys: list[dict[str, Any]]) -> dict[str, Any]: + """Build a JWKS document for ``/.well-known/jwks.json``. + + Example:: + + from agentscore_commerce.identity.ucp_jwks import build_jwks_response + + @app.get('/.well-known/jwks.json') + async def jwks(): + return build_jwks_response([key.public_jwk]) + """ + return {"keys": keys} + + +__all__ = [ + "GeneratedUCPKey", + "UCPVerificationError", + "build_jwks_response", + "generate_ucp_signing_key", + "sign_ucp_profile", + "verify_ucp_profile", +] diff --git a/examples/README.md b/examples/README.md index b42ee37..0a10827 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,13 +4,13 @@ Runnable, copy-pasteable example integrations covering the most common merchant | Example | Scenario | What it shows | |---|---|---| -| [`identity_only.py`](./identity_only.py) | Compliance gate without payment | Minimal — wraps any endpoint with KYC + age + jurisdiction checks. Vendor handles their own payment. | +| [`identity_only.py`](./identity_only.py) | Compliance gate without payment | Minimal: wraps any endpoint with KYC + age + jurisdiction checks. Vendor handles their own payment. | | [`api_provider.py`](./api_provider.py) | API provider (Exa-style) | Per-call billing on multiple rails: Tempo MPP + x402 (Base + Solana). Discovery probe responder + multi-rail 402 challenge. No identity gate. | | [`multi_rail_merchant.py`](./multi_rail_merchant.py) | Full agent-commerce merchant | Identity gate + Tempo MPP + x402 (Base + Solana) + Stripe SPT, all rails accepted, full 402 builder using `build_402_body` + `build_accepted_methods` + `build_how_to_pay` + `build_agent_instructions`. | | [`stripe_multichain_merchant.py`](./stripe_multichain_merchant.py) | Stripe-anchored multi-chain | Stripe PaymentIntent with deposit_options for tempo/base/solana; crypto deposits flow through Stripe. Includes testnet `simulate_crypto_deposit` helper. | | [`variable_cost_merchant.py`](./variable_cost_merchant.py) | Pay-per-actual-usage (LLM, transcode, etc.) | Same use case on **two protocols**: x402 upto (Permit2 authorize-max → `Settlement-Overrides` settle-actual) AND MPP tempo session (channel + SSE + mid-stream vouchers). Vendor offers both. | | [`compliance_merchant.py`](./compliance_merchant.py) | Regulated-goods merchant (wine, cannabis, etc.) | Full compliance gate + custom `on_denied` composing commerce helpers: `verification_agent_instructions`, `is_fixable_denial`, `build_contact_support_next_steps`, `denial_reason_to_body`/`denial_reason_status`, `build_signer_mismatch_body`. Shows how vendors write only the business-specific branches and let commerce handle the rest. | -| [`per_product_policy_merchant.py`](./per_product_policy_merchant.py) | Multi-product merchant with mixed compliance needs | One product carries a hard gate (wine — KYC + 21 + US-state allowlist), another has no gate at all (anonymous merch, ships anywhere), a third uses `enforcement="soft"` (request KYC as a fraud signal but accept anonymous sales, stamping `identity_status="unverified"` on the order). Uses `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`. | +| [`per_product_policy_merchant.py`](./per_product_policy_merchant.py) | Multi-product merchant with mixed compliance needs | One product carries a hard gate (wine: KYC + 21 + US-state allowlist), another has no gate at all (anonymous merch, ships anywhere), a third uses `enforcement="soft"` (request KYC as a fraud signal but accept anonymous sales, stamping `identity_status="unverified"` on the order). Uses `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`. | ## How to use @@ -19,7 +19,7 @@ Runnable, copy-pasteable example integrations covering the most common merchant 3. Install peer deps mentioned at the top of the file (only what you actually need) 4. Set the env vars listed at the top of the file 5. Run with `uvicorn examples.:app --port 3000` -6. Iterate — these are templates, not frameworks +6. Iterate; these are templates, not frameworks ## Patterns @@ -39,16 +39,15 @@ These examples are intentionally thin on domain logic. Vendors plug in their own - Order storage (Postgres, durable queue, etc.) - Customer email / fulfillment notifications - Tax / shipping calculators -- Frontend UI (none of these examples include one — they're agent-only APIs) +- Frontend UI (none of these examples include one; they're agent-only APIs) AgentScore Commerce handles the agent commerce protocol layer; everything else is your business. ## Differences from node-commerce examples -Python doesn't have peer-dep equivalents for `@x402/core`, `@x402/evm`, `@solana/mpp`, or `mppx` — those are TypeScript-only ecosystems today. Three implications: +Python wraps `x402[evm]` and `pympp[server,tempo,stripe]` as peer deps; `@solana/mpp` has no Python equivalent today. Two implications: -1. **No `create_x402_server` / `create_mppx_server` factories.** The commerce package exposes `register_x402_schemes_v1_v2` for the x402 v1+v2 dispatch helper, but happy-path setup (registering the facilitator, schemes, etc.) is something Python merchants do via direct HTTP calls to their facilitator of choice. -2. **`extract_payment_signer` returns EVM only.** Solana SPL Token payer recovery requires a Solana SDK (`solders` / `solana-py`) which isn't bundled. Pass the recovered Solana payer via `signer=...` to `verify_wallet_signer_match` directly. -3. **Streaming session payments (variable_cost_merchant.py)** sketches the protocol but doesn't ship a working tempo session implementation — there's no pip-installable `mppx` equivalent. The example shows the response shape; vendors using session payments today should check the [tempo session protocol docs](https://mpp.dev/guides/streamed-payments) and bind to a Solana wallet library directly. +1. **`extract_payment_signer` returns EVM only.** Solana SPL Token payer recovery requires a Solana SDK (`solders` / `solana-py`) which isn't bundled. Pass the recovered Solana payer via `signer=...` to `verify_wallet_signer_match` directly. +2. **Streaming session payments (variable_cost_merchant.py)** sketches the protocol but doesn't ship a working tempo session implementation; there's no pip-installable `mppx` equivalent. The example shows the response shape; vendors using session payments today should check the [tempo session protocol docs](https://mpp.dev/guides/streamed-payments) and bind to a Solana wallet library directly. -For Python merchants on x402 alone (Base or Solana), every other helper (directives, headers, dispatch, settle-overrides, signer extraction for EVM, accepted_methods, agent_instructions, how_to_pay) is fully native. +For Python merchants on x402 alone (Base or Solana), every helper (`create_x402_server`, `create_mppx_server`, directives, headers, dispatch, settle-overrides, signer extraction for EVM, accepted_methods, agent_instructions, how_to_pay) is fully native. diff --git a/examples/signed_ucp_merchant.py b/examples/signed_ucp_merchant.py new file mode 100644 index 0000000..6a53811 --- /dev/null +++ b/examples/signed_ucp_merchant.py @@ -0,0 +1,173 @@ +"""Signed UCP profile example — ``/.well-known/ucp`` + ``/.well-known/jwks.json``. + +AgentScore's ``agentscore-profile+jws`` is a vendor extension layered on top of +the UCP profile for trust-mode verifiers (Visa AP2 pilots, regulated-commerce +verifiers) that opt into auditable cryptographic provenance. UCP §6 itself does +NOT mandate profile-body signing — Pura Vida and other Shopify-backed UCP +merchants ship unsigned in production today, and live UCP-aware agents (Google +AI Mode, Gemini commerce, Microsoft Copilot, Perplexity) accept unsigned +profiles. This example wires both routes against a persistent signing key +(env-loaded for prod, ephemeral for dev) for verifiers that DO opt into the +signed envelope. + +Run:: + + uv run uvicorn examples.signed_ucp_merchant:app --port 3010 + +Production checklist: + +* Set ``UCP_SIGNING_KEY_JWK_PRIVATE`` to a JSON-encoded private JWK (mint via + :func:`generate_ucp_signing_key` once, persist in your secret manager). +* The kid in the env JWK MUST match what verifiers will see in your published + profile — pick a stable name like ``merchant-2026-05``. +* Configure ``Cache-Control: public, max-age=300`` (or longer) on + ``/.well-known/jwks.json`` so verifiers don't hammer the endpoint. +* Rotate by minting a new key + new kid, publishing both in the JWKS, signing + new profiles with the new key, then dropping the old JWK after your verifier + cache TTL expires. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import os +from typing import Any, Literal + +from fastapi import FastAPI +from fastapi.responses import JSONResponse + +from agentscore_commerce.identity import ( + UCPPaymentHandlerBinding, + UCPServiceBinding, + UCPSigningKey, + UCPVerificationError, + build_jwks_response, + build_ucp_profile, + generate_ucp_signing_key, + sign_ucp_profile, + verify_ucp_profile, +) +from agentscore_commerce.identity.ucp_jwks import GeneratedUCPKey + +logger = logging.getLogger("signed_ucp_merchant") + +KID = os.environ.get("UCP_SIGNING_KEY_KID", "merchant-2026-05") +ALG: Literal["EdDSA", "ES256"] = "ES256" if os.environ.get("UCP_SIGNING_KEY_ALG") == "ES256" else "EdDSA" + +# Asyncio lock + cached Future so concurrent first-callers don't generate +# different keys (race condition fix). +_lock = asyncio.Lock() +_cached: GeneratedUCPKey | None = None + + +async def load_signing_key() -> GeneratedUCPKey: + global _cached + async with _lock: + if _cached is not None: + return _cached + env_jwk = os.environ.get("UCP_SIGNING_KEY_JWK_PRIVATE") + if env_jwk: + from joserfc.jwk import ECKey, OKPKey # type: ignore[import-not-found] + + try: + jwk_dict = json.loads(env_jwk) + except json.JSONDecodeError as exc: + msg = f"UCP_SIGNING_KEY_JWK_PRIVATE is not valid JSON: {exc}" + raise ValueError(msg) from exc + # Detect alg from JWK shape; ignore env if it conflicts. + kty = jwk_dict.get("kty") + crv = jwk_dict.get("crv") + if kty == "OKP" and crv == "Ed25519": + priv = OKPKey.import_key(jwk_dict) + effective_alg: Literal["EdDSA", "ES256"] = "EdDSA" + elif kty == "EC" and crv == "P-256": + priv = ECKey.import_key(jwk_dict) + effective_alg = "ES256" + else: + msg = f"Unsupported env JWK: kty={kty} crv={crv}" + raise ValueError(msg) + public_jwk: dict[str, Any] = priv.as_dict(private=False) + public_jwk.setdefault("kid", jwk_dict.get("kid", KID)) + public_jwk["alg"] = effective_alg + public_jwk["use"] = "sig" + _cached = GeneratedUCPKey(private_key=priv, public_jwk=public_jwk) + return _cached + logger.warning( + "UCP_SIGNING_KEY_JWK_PRIVATE not set — generating ephemeral key. " + "Verifier caches will break across restarts." + ) + _cached = generate_ucp_signing_key(kid=KID, alg=ALG) + return _cached + + +app = FastAPI() + + +@app.get("/.well-known/ucp") +async def well_known_ucp() -> JSONResponse: + key = await load_signing_key() + profile = build_ucp_profile( + name="My Agent Service", + services={ + "dev.ucp.shopping": [ + UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/2026-04-08/specification/overview", + transport="mcp", + endpoint="https://agents.example.com/api/ucp/mcp", + schema="https://ucp.dev/services/shopping/openrpc.json", + ), + ], + }, + payment_handlers={ + "sh.agentscore.payment.tempo": [ + UCPPaymentHandlerBinding( + id="tempo", + version="2026-04-08", + spec="https://agentscore.sh/specification/payment-handlers/tempo", + schema="https://agentscore.sh/schemas/payment-handlers/tempo.json", + config={"recipient": "0xfeedface"}, + ), + ], + }, + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + ) + signed = sign_ucp_profile( + profile.to_dict(), + signing_key=key.private_key, + kid=key.public_jwk["kid"], + alg=key.public_jwk.get("alg", ALG), + ) + return JSONResponse(signed, headers={"Cache-Control": "public, max-age=60"}) + + +@app.get("/.well-known/jwks.json") +async def well_known_jwks() -> JSONResponse: + key = await load_signing_key() + return JSONResponse( + build_jwks_response([key.public_jwk]), + headers={ + "Cache-Control": "public, max-age=300", + "Content-Type": "application/jwk-set+json", + }, + ) + + +@app.get("/_selftest/ucp") +async def selftest() -> JSONResponse: + """Local round-trip: sign+serve+fetch+verify, return UCPVerificationError code on failure.""" + profile_resp = await well_known_ucp() + jwks_resp = await well_known_jwks() + profile = json.loads(profile_resp.body.decode()) + jwks = json.loads(jwks_resp.body.decode()) + try: + verify_ucp_profile(profile, jwks) + return JSONResponse({"ok": True, "kid": profile["signing_keys"][0]["kid"]}) + except UCPVerificationError as exc: + logger.exception("UCP self-test verification failed") + return JSONResponse( + {"ok": False, "code": exc.code, "error": type(exc).__name__}, + status_code=500, + ) diff --git a/pyproject.toml b/pyproject.toml index ca31255..b3cb966 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentscore-commerce" -version = "1.3.6" +version = "1.4.0" description = "Agent commerce SDK for Python — identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce." readme = "README.md" license = "MIT" @@ -43,6 +43,7 @@ stripe = ["stripe>=11.0.0"] x402 = ["x402[evm,fastapi]>=2.9,<3"] mppx = ["pympp[server,tempo,stripe]>=0.6,<1"] coinbase = ["cdp-sdk>=1.0,<2"] +ucp = ["joserfc>=1.0.0,<2"] [project.urls] Homepage = "https://agentscore.sh" @@ -70,6 +71,7 @@ dev = [ "stripe>=11.0.0", "lefthook>=2.1.6", "cdp-sdk>=1.0,<2", + "joserfc>=1.0.0,<2", ] [tool.ty.src] @@ -78,3 +80,11 @@ include = ["agentscore_commerce"] [tool.pytest.ini_options] asyncio_mode = "auto" addopts = "--cov=agentscore_commerce --cov-report=term-missing --cov-fail-under=95" +# joserfc emits SecurityWarning for EdDSA on every sign/verify (see RFC 9864). +# UCP §6 explicitly accepts EdDSA so we suppress. SDK helpers wrap their own +# joserfc calls with a context-manager filter, but tests that hand-construct +# joserfc calls directly need the suppression too. Scope matches the exact +# message + class so a future unrelated EdDSA warning still surfaces. +filterwarnings = [ + "ignore:^EdDSA is deprecated via RFC 9864$:joserfc.errors.SecurityWarning", +] diff --git a/scripts/regenerate_cross_lang_fixtures.py b/scripts/regenerate_cross_lang_fixtures.py new file mode 100644 index 0000000..49c41d9 --- /dev/null +++ b/scripts/regenerate_cross_lang_fixtures.py @@ -0,0 +1,322 @@ +"""Regenerate the full cross-lang fixture corpus (Python side). + +Writes all ``py-*.json`` fixtures under ``tests/fixtures/cross-lang/``. Used after a +canonicalization-relevant change (typ rename, capability-name rename, schema-URL +rename, key-sort tweak, profile-shape change) where every JWS in the corpus needs +to be re-signed. + +Each scenario hand-crafts the profile body using the spec-compliant input shape +(``services`` / ``capabilities`` / ``payment_handlers`` as MAPS keyed by reverse-DNS +service / capability / handler name), signs with a fresh keypair, and writes the +``{profile, jwks, alg, kid, generator}`` envelope. Cross-lang verify in +``tests/test_ucp_cross_lang.py`` (and the Node sibling) pulls these in alongside the +``node-*`` fixtures generated by the Node sibling. + +Run: ``uv run python scripts/regenerate_cross_lang_fixtures.py`` +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from agentscore_commerce.identity import ( + AssessResult, + OperatorVerification, + UCPCapabilityBinding, + UCPPaymentHandlerBinding, + UCPServiceBinding, + UCPSigningKey, + build_ucp_profile, +) +from agentscore_commerce.identity.ucp_jwks import ( + build_jwks_response, + generate_ucp_signing_key, + sign_ucp_profile, +) + +OUT_DIR = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "cross-lang" + + +def _write(name: str, env: dict[str, Any]) -> None: + out = OUT_DIR / f"{name}.json" + out.write_text(json.dumps(env, indent=2, ensure_ascii=False) + "\n") + print(f"wrote {out}") + + +def _envelope(signed: dict[str, Any], public_jwk: dict[str, Any], alg: str, kid: str) -> dict[str, Any]: + return { + "profile": signed, + "jwks": build_jwks_response([public_jwk]), + "alg": alg, + "kid": kid, + "generator": "python", + } + + +# Spec-compliant binding helpers — each scenario uses these (or variants) so the +# fixtures cover the full set of canonical UCP fields per binding type. + + +def _shop_service_mcp(host: str) -> UCPServiceBinding: + return UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/2026-04-08/specification/overview", + transport="mcp", + endpoint=f"{host}/api/ucp/mcp", + schema="https://ucp.dev/services/shopping/openrpc.json", + ) + + +def _shop_service_a2a(host: str) -> UCPServiceBinding: + return UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/2026-04-08/specification/overview", + transport="a2a", + endpoint=f"{host}/.well-known/agent-card.json", + ) + + +def _tempo_handler(config: dict[str, Any] | None = None) -> UCPPaymentHandlerBinding: + h = UCPPaymentHandlerBinding( + id="tempo", + version="2026-04-08", + spec="https://agentscore.sh/specification/payment-handlers/tempo", + schema="https://agentscore.sh/schemas/payment-handlers/tempo.json", + ) + if config is not None: + h.config = config + return h + + +def _x402_handler(networks: list[str]) -> UCPPaymentHandlerBinding: + return UCPPaymentHandlerBinding( + id="x402", + version="2026-04-08", + spec="https://agentscore.sh/specification/payment-handlers/x402", + schema="https://agentscore.sh/schemas/payment-handlers/x402.json", + config={"networks": networks}, + ) + + +def _stripe_handler(config: dict[str, Any]) -> UCPPaymentHandlerBinding: + return UCPPaymentHandlerBinding( + id="stripe", + version="2026-04-08", + spec="https://agentscore.sh/specification/payment-handlers/stripe-spt", + schema="https://agentscore.sh/schemas/payment-handlers/stripe-spt.json", + config=config, + ) + + +def main() -> None: + # py-minimal — empty maps; just metadata + signing keys. + kid = "py-minimal-EdDSA" + key = generate_ucp_signing_key(kid=kid) + profile = build_ucp_profile( + services={"dev.ucp.shopping": [_shop_service_mcp("https://m.example.com")]}, + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + name="Minimal Merchant", + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-minimal", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-es256-rails — multi-transport service + multi-rail + ES256 signing key. + kid = "py-es256-rails-ES256" + key = generate_ucp_signing_key(kid=kid, alg="ES256") + profile = build_ucp_profile( + services={ + "dev.ucp.shopping": [ + _shop_service_mcp("https://a.example.com"), + _shop_service_a2a("https://a.example.com"), + ], + }, + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + payment_handlers={ + "sh.agentscore.payment.tempo": [_tempo_handler({"rail": "tempo-mainnet", "chain_id": 4217})], + "sh.agentscore.payment.x402": [_x402_handler(["base-8453"])], + }, + name="ES256 Merchant", + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid, alg="ES256") + _write("py-es256-rails", _envelope(signed, key.public_jwk, "ES256", kid)) + + # py-extras-int — payment_handler config with int + string fields. + kid = "py-extras-int-EdDSA" + key = generate_ucp_signing_key(kid=kid) + profile = build_ucp_profile( + services={"dev.ucp.shopping": [_shop_service_mcp("https://e.example.com")]}, + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + payment_handlers={ + "sh.agentscore.payment.stripe-spt": [_stripe_handler({"profile_id": "abc", "count": 7})], + }, + name="Extras Merchant", + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-extras-int", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-capability — hand-crafted vendor capability under sh.agentscore.identity. + kid = "py-capability-EdDSA" + key = generate_ucp_signing_key(kid=kid) + custom_capability = UCPCapabilityBinding( + version="1", + spec="https://agentscore.sh/specification/identity", + schema="https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + # `extras` flat on the binding — kyc_required is a vendor field on this binding. + extras={"kyc_required": True}, + ) + profile = build_ucp_profile( + services={"dev.ucp.shopping": [_shop_service_mcp("https://c.example.com")]}, + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + capabilities={"sh.agentscore.identity": [custom_capability]}, + payment_handlers={ + "sh.agentscore.payment.tempo": [_tempo_handler({"rail": "tempo-mainnet", "chain_id": 4217})], + }, + name="Capability Merchant", + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-capability", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-unicode — multi-byte UTF-8 in name / endpoint / config. + kid = "py-unicode-EdDSA" + key = generate_ucp_signing_key(kid=kid) + profile = build_ucp_profile( + services={"dev.ucp.shopping": [_shop_service_mcp("https://日本.example.com")]}, + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + payment_handlers={ + "sh.agentscore.payment.tempo": [_tempo_handler({"note": "メモ"})], + }, + name="Café 日本 🍷 Merchant", + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-unicode", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-multikey — JWKS with two keys, signed by the newer one. + old_key = generate_ucp_signing_key(kid="py-multikey-old") + new_key = generate_ucp_signing_key(kid="py-multikey-new") + profile = build_ucp_profile( + services={"dev.ucp.shopping": [_shop_service_mcp("https://mk.example.com")]}, + signing_keys=[ + UCPSigningKey.from_jwk(old_key.public_jwk), + UCPSigningKey.from_jwk(new_key.public_jwk), + ], + payment_handlers={ + "sh.agentscore.payment.tempo": [_tempo_handler({"rail": "tempo-mainnet"})], + }, + name="Multi-Key Merchant", + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=new_key.private_key, kid="py-multikey-new") + _write( + "py-multikey", + { + "profile": signed, + "jwks": build_jwks_response([old_key.public_jwk, new_key.public_jwk]), + "alg": "EdDSA", + "kid": "py-multikey-new", + "generator": "python", + }, + ) + + # py-emoji-keys — extras at top-level (outside the `ucp` envelope) with non-ASCII + # object keys (BMP private use, CJK compatibility, supplementary plane). + # Exercises codepoint-vs-UTF-16 sort. + kid = "py-emoji-keys-EdDSA" + key = generate_ucp_signing_key(kid=kid) + profile = build_ucp_profile( + services={"dev.ucp.shopping": [_shop_service_mcp("https://emoji.example.com")]}, + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + payment_handlers={ + "sh.agentscore.payment.tempo": [_tempo_handler()], + }, + name="Emoji Keys Merchant", + extras={ + "a": 1, + "豈": 2, + "": 3, + "🍷": 4, + }, + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-emoji-keys", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-int-boundary — exercises Number.MAX_SAFE_INTEGER round-trip via top-level extras. + kid = "py-int-boundary-EdDSA" + key = generate_ucp_signing_key(kid=kid) + profile = build_ucp_profile( + services={"dev.ucp.shopping": [_shop_service_mcp("https://i.example.com")]}, + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + name="Int Boundary Merchant", + extras={ + "max_safe_int": 9007199254740991, + "min_safe_int": -9007199254740991, + "small_int": 42, + "neg_small_int": -42, + "zero": 0, + }, + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-int-boundary", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-data-driven-claims — exercises build_ucp_profile data path with API-shape + # "missing" sentinels (empty string + None). Both languages MUST emit identical + # canonical bytes for this input. + kid = "py-data-driven-claims-EdDSA" + key = generate_ucp_signing_key(kid=kid) + result = AssessResult( + allow=True, + resolved_operator="op_data_driven", + verify_url="https://agentscore.sh/verify/op_data_driven", + raw={ + "account_verification": { + "kyc_level": "", + "sanctions_clear": False, + "age_bracket": None, + "jurisdiction": None, + "verified_at": None, + }, + }, + ) + profile = build_ucp_profile( + services={"dev.ucp.shopping": [_shop_service_mcp("https://d.example.com")]}, + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + name="Data Driven Claims Merchant", + data=result, + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-data-driven-claims", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-typed-claims — exercises typed AssessResult fields (no raw fallback). + # Cross-lang parity check for the typed-field-only call site. + kid = "py-typed-claims-EdDSA" + key = generate_ucp_signing_key(kid=kid) + result = AssessResult( + allow=True, + resolved_operator="op_typed_claims", + verify_url="https://agentscore.sh/verify/op_typed_claims", + operator_verification=OperatorVerification( + level="enhanced", + operator_type="api", + verified_at="2026-04-01T00:00:00Z", + ), + account_verification={ + "kyc_level": "enhanced", + "sanctions_clear": True, + "age_bracket": "21+", + "jurisdiction": "US", + "verified_at": "2026-04-01T00:00:00Z", + }, + raw=None, + ) + profile = build_ucp_profile( + services={"dev.ucp.shopping": [_shop_service_mcp("https://t.example.com")]}, + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + name="Typed Claims Merchant", + data=result, + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-typed-claims", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/cross-lang/node-capability.json b/tests/fixtures/cross-lang/node-capability.json new file mode 100644 index 0000000..a06e437 --- /dev/null +++ b/tests/fixtures/cross-lang/node-capability.json @@ -0,0 +1,69 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://c.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "kyc_required": true + } + ] + }, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + } + ] + }, + "name": "Capability Merchant" + }, + "signing_keys": [ + { + "kid": "node-capability-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "IuEDuQu_5--c_GVEaY4x0xjGbKro965U5VGyRY8TxpI" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJJdUVEdVF1XzUtLWNfR1ZFYVk0eDB4akdiS3JvOTY1VTVWR3lSWThUeHBJIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIxIn1dfSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.s36lpaOS-eGdTC0agCpLU_JxDLNO6nM5YjOTxJb6JoYVYzWBaflJCkWxwN6bDgdgDh-lPSY7_l7X0636TjpzCA" + }, + "jwks": { + "keys": [ + { + "kid": "node-capability-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "IuEDuQu_5--c_GVEaY4x0xjGbKro965U5VGyRY8TxpI" + } + ] + }, + "alg": "EdDSA", + "kid": "node-capability-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-data-driven-claims.json b/tests/fixtures/cross-lang/node-data-driven-claims.json new file mode 100644 index 0000000..c59119e --- /dev/null +++ b/tests/fixtures/cross-lang/node-data-driven-claims.json @@ -0,0 +1,69 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://d.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ], + "claims": { + "operator_id": "op_data_driven", + "kyc_level": "none", + "sanctions_clear": false, + "age_bracket": "unknown", + "jurisdiction": "", + "verified_at": null, + "verify_url": "https://agentscore.sh/verify/op_data_driven", + "issuer": "https://agentscore.sh" + } + } + ] + }, + "payment_handlers": {}, + "name": "Data Driven Claims Merchant" + }, + "signing_keys": [ + { + "kid": "node-data-driven-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "t9ul3BiA3r0fugZcbcEcyARb8SAH_-4dalE3sjaVMKc" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InQ5dWwzQmlBM3IwZnVnWmNiY0VjeUFSYjhTQUhfLTRkYWxFM3NqYVZNS2MifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnsic2guYWdlbnRzY29yZS5pZGVudGl0eSI6W3siY2xhaW1zIjp7ImFnZV9icmFja2V0IjoidW5rbm93biIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IiIsImt5Y19sZXZlbCI6Im5vbmUiLCJvcGVyYXRvcl9pZCI6Im9wX2RhdGFfZHJpdmVuIiwic2FuY3Rpb25zX2NsZWFyIjpmYWxzZSwidmVyaWZpZWRfYXQiOm51bGwsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX2RhdGFfZHJpdmVuIn0sImV4dGVuZHMiOlsiZGV2LnVjcC5zaG9wcGluZy5jaGVja291dCIsImRldi51Y3Auc2hvcHBpbmcuY2FydCJdLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL2lkZW50aXR5IiwidmVyc2lvbiI6IjEifV19LCJuYW1lIjoiRGF0YSBEcml2ZW4gQ2xhaW1zIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6e30sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9kLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.8MSGbttC6ITB1vEYr0Wq8kSRniYAV25gMT7jahMGKIJfcE-rBGTukPFpXzpBWUNkSWOW4ihkOvTA5Wxws4NjDA" + }, + "jwks": { + "keys": [ + { + "kid": "node-data-driven-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "t9ul3BiA3r0fugZcbcEcyARb8SAH_-4dalE3sjaVMKc" + } + ] + }, + "alg": "EdDSA", + "kid": "node-data-driven-claims-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-emoji-keys.json b/tests/fixtures/cross-lang/node-emoji-keys.json new file mode 100644 index 0000000..ca325f0 --- /dev/null +++ b/tests/fixtures/cross-lang/node-emoji-keys.json @@ -0,0 +1,60 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://emoji.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json" + } + ] + }, + "name": "Emoji Keys Merchant" + }, + "signing_keys": [ + { + "kid": "node-emoji-keys-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "O5o3d9qQsgo-eDXV9rnt-saHwzpiitL4kTcVxGr6mjE" + } + ], + "a": 1, + "豈": 2, + "": 3, + "🍷": 4, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyIiOjMsImEiOjEsInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ik81bzNkOXFRc2dvLWVEWFY5cm50LXNhSHd6cGlpdEw0a1RjVnhHcjZtakUifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnt9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifSwi6LGIIjoyLCLwn423Ijo0fQ.a-34-eGa5zJtMxXiefamLIcm4UM_Wix1XpHcJRXcM8Fs1Lx3ErLxLl-pdgyveDP1DVel7FmaSXJJuANSRvB4Bw" + }, + "jwks": { + "keys": [ + { + "kid": "node-emoji-keys-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "O5o3d9qQsgo-eDXV9rnt-saHwzpiitL4kTcVxGr6mjE" + } + ] + }, + "alg": "EdDSA", + "kid": "node-emoji-keys-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-es256-rails.json b/tests/fixtures/cross-lang/node-es256-rails.json new file mode 100644 index 0000000..c8bd844 --- /dev/null +++ b/tests/fixtures/cross-lang/node-es256-rails.json @@ -0,0 +1,81 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://a.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + }, + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "a2a", + "endpoint": "https://a.example.com/.well-known/agent-card.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + } + ], + "sh.agentscore.payment.x402": [ + { + "id": "x402", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/x402", + "schema": "https://agentscore.sh/schemas/payment-handlers/x402.json", + "config": { + "networks": [ + "base-8453" + ] + } + } + ] + }, + "name": "ES256 Merchant" + }, + "signing_keys": [ + { + "kid": "node-es256-rails-ES256", + "alg": "ES256", + "use": "sig", + "crv": "P-256", + "kty": "EC", + "x": "YJlpUMxCjw_uFVaklMcPBroRAAyWRFBb6hogNbBzwqc", + "y": "RPRH4k6hBTqEX0-Wf9s2y3VAFcwtYDnZz53Y-3G-Vl8" + } + ], + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVTMjU2IiwiY3J2IjoiUC0yNTYiLCJraWQiOiJub2RlLWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiWUpscFVNeENqd191RlZha2xNY1BCcm9SQUF5V1JGQmI2aG9nTmJCendxYyIsInkiOiJSUFJINGs2aEJUcUVYMC1XZjlzMnkzVkFGY3d0WURuWno1M1ktM0ctVmw4In1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkVTMjU2IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJjaGFpbl9pZCI6NDIxNywicmFpbCI6InRlbXBvLW1haW5uZXQifSwiaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV0sInNoLmFnZW50c2NvcmUucGF5bWVudC54NDAyIjpbeyJjb25maWciOnsibmV0d29ya3MiOlsiYmFzZS04NDUzIl19LCJpZCI6Ing0MDIiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3g0MDIuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3g0MDIiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9hLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifSx7ImVuZHBvaW50IjoiaHR0cHM6Ly9hLmV4YW1wbGUuY29tLy53ZWxsLWtub3duL2FnZW50LWNhcmQuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoiYTJhIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.lhet7Dek3XSboG8lxyoGEc4-6kEQqwkxXbR2qqKGdKlB4aoXmHrN0hpQZSzzfqKpjwN_I7VgZiKZOteTqhKrMQ" + }, + "jwks": { + "keys": [ + { + "kid": "node-es256-rails-ES256", + "alg": "ES256", + "use": "sig", + "crv": "P-256", + "kty": "EC", + "x": "YJlpUMxCjw_uFVaklMcPBroRAAyWRFBb6hogNbBzwqc", + "y": "RPRH4k6hBTqEX0-Wf9s2y3VAFcwtYDnZz53Y-3G-Vl8" + } + ] + }, + "alg": "ES256", + "kid": "node-es256-rails-ES256", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-extras-int.json b/tests/fixtures/cross-lang/node-extras-int.json new file mode 100644 index 0000000..60f17ec --- /dev/null +++ b/tests/fixtures/cross-lang/node-extras-int.json @@ -0,0 +1,60 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://e.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.stripe-spt": [ + { + "id": "stripe", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/stripe-spt", + "schema": "https://agentscore.sh/schemas/payment-handlers/stripe-spt.json", + "config": { + "profile_id": "abc", + "count": 7 + } + } + ] + }, + "name": "Extras Merchant" + }, + "signing_keys": [ + { + "kid": "node-extras-int-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "LwGeYhxjsedo9kllWo8uRdHZnf9teSPjEGLJrhF9o0M" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJMd0dlWWh4anNlZG85a2xsV284dVJkSFpuZjl0ZVNQakVHTEpyaEY5bzBNIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkV4dHJhcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnN0cmlwZS1zcHQiOlt7ImNvbmZpZyI6eyJjb3VudCI6NywicHJvZmlsZV9pZCI6ImFiYyJ9LCJpZCI6InN0cmlwZSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvc3RyaXBlLXNwdC5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvc3RyaXBlLXNwdCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL2UuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In19.s6sBki-bBhRuZNZiv7s7NO3NpLDfhoXhZXKcK2uitVGiBh9nANv-pi8L-nAIBte8jN_DFoeqtWQJiAK188XqBg" + }, + "jwks": { + "keys": [ + { + "kid": "node-extras-int-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "LwGeYhxjsedo9kllWo8uRdHZnf9teSPjEGLJrhF9o0M" + } + ] + }, + "alg": "EdDSA", + "kid": "node-extras-int-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-int-boundary.json b/tests/fixtures/cross-lang/node-int-boundary.json new file mode 100644 index 0000000..74dd819 --- /dev/null +++ b/tests/fixtures/cross-lang/node-int-boundary.json @@ -0,0 +1,52 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://i.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": {}, + "name": "Int Boundary Merchant" + }, + "signing_keys": [ + { + "kid": "node-int-boundary-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "vmNTcQKo5jUIpTVnWRSkLu-s7cUoNO_OfPJTctAOhR4" + } + ], + "max_safe_int": 9007199254740991, + "min_safe_int": -9007199254740991, + "small_int": 42, + "neg_small_int": -42, + "zero": 0, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5lZ19zbWFsbF9pbnQiOi00Miwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWludC1ib3VuZGFyeS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJ2bU5UY1FLbzVqVUlwVFZuV1JTa0x1LXM3Y1VvTk9fT2ZQSlRjdEFPaFI0In1dLCJzbWFsbF9pbnQiOjQyLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkludCBCb3VuZGFyeSBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vaS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifSwiemVybyI6MH0.iIsGfdlC2ZqMh3ouvz86u4QmGS0d-JR9KyTcUNoMTnqbt0P63PBJ7lXCoZ64DY4XtFJ83sPzSrOIzvdsbrOvBQ" + }, + "jwks": { + "keys": [ + { + "kid": "node-int-boundary-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "vmNTcQKo5jUIpTVnWRSkLu-s7cUoNO_OfPJTctAOhR4" + } + ] + }, + "alg": "EdDSA", + "kid": "node-int-boundary-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-minimal.json b/tests/fixtures/cross-lang/node-minimal.json new file mode 100644 index 0000000..32eef05 --- /dev/null +++ b/tests/fixtures/cross-lang/node-minimal.json @@ -0,0 +1,47 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://m.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": {}, + "name": "Minimal Merchant" + }, + "signing_keys": [ + { + "kid": "node-minimal-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "69RWgrarCEN0sSH5FfkJ2-miQQNRpXYh0wt9kviqzqk" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI2OVJXZ3JhckNFTjBzU0g1RmZrSjItbWlRUU5ScFhZaDB3dDlrdmlxenFrIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6Ik1pbmltYWwgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7fSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL20uZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In19.ei7hxM6v-gnxAkgG4NiWLwzhd9wOxg3lO9ZTFVEuSBaAho0n_GaQayO99ibjQgqa2yUa1J9PcGh3woMh7cQcAA" + }, + "jwks": { + "keys": [ + { + "kid": "node-minimal-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "69RWgrarCEN0sSH5FfkJ2-miQQNRpXYh0wt9kviqzqk" + } + ] + }, + "alg": "EdDSA", + "kid": "node-minimal-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-multikey.json b/tests/fixtures/cross-lang/node-multikey.json new file mode 100644 index 0000000..5027299 --- /dev/null +++ b/tests/fixtures/cross-lang/node-multikey.json @@ -0,0 +1,75 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://mk.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet" + } + } + ] + }, + "name": "Multi-Key Merchant" + }, + "signing_keys": [ + { + "kid": "node-multikey-old", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "dh_cI_8_Z79h3t5i72fKw89EwpeJiA2ELN1SnS_OgdQ" + }, + { + "kid": "node-multikey-new", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "oxGfu9h6LckqvQ0eVkovSzUwCdGo8xLkPcq8siUoh7M" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbXVsdGlrZXktb2xkIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImRoX2NJXzhfWjc5aDN0NWk3MmZLdzg5RXdwZUppQTJFTE4xU25TX09nZFEifSx7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Im94R2Z1OWg2TGNrcXZRMGVWa292U3pVd0NkR284eExrUGNxOHNpVW9oN00ifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnt9LCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.fEq5VVrBtuwEYJGcpHuaTCVQWmS6LvcOdtS-reZGyLFosCrmok9eU86w9m79aO6k0u_CXOC_n90TvbFfuKINCA" + }, + "jwks": { + "keys": [ + { + "kid": "node-multikey-old", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "dh_cI_8_Z79h3t5i72fKw89EwpeJiA2ELN1SnS_OgdQ" + }, + { + "kid": "node-multikey-new", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "oxGfu9h6LckqvQ0eVkovSzUwCdGo8xLkPcq8siUoh7M" + } + ] + }, + "alg": "EdDSA", + "kid": "node-multikey-new", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-typed-claims.json b/tests/fixtures/cross-lang/node-typed-claims.json new file mode 100644 index 0000000..d754f4c --- /dev/null +++ b/tests/fixtures/cross-lang/node-typed-claims.json @@ -0,0 +1,69 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://t.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ], + "claims": { + "operator_id": "op_typed_claims", + "kyc_level": "enhanced", + "sanctions_clear": true, + "age_bracket": "21+", + "jurisdiction": "US", + "verified_at": "2026-04-01T00:00:00Z", + "verify_url": "https://agentscore.sh/verify/op_typed_claims", + "issuer": "https://agentscore.sh" + } + } + ] + }, + "payment_handlers": {}, + "name": "Typed Claims Merchant" + }, + "signing_keys": [ + { + "kid": "node-typed-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "6JcesuEfiy104P6W8zOsruWkL7Ju7RLXMyR2F3fQ4xM" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjZKY2VzdUVmaXkxMDRQNlc4ek9zcnVXa0w3SnU3UkxYTXlSMkYzZlE0eE0ifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnsic2guYWdlbnRzY29yZS5pZGVudGl0eSI6W3siY2xhaW1zIjp7ImFnZV9icmFja2V0IjoiMjErIiwiaXNzdWVyIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoIiwianVyaXNkaWN0aW9uIjoiVVMiLCJreWNfbGV2ZWwiOiJlbmhhbmNlZCIsIm9wZXJhdG9yX2lkIjoib3BfdHlwZWRfY2xhaW1zIiwic2FuY3Rpb25zX2NsZWFyIjp0cnVlLCJ2ZXJpZmllZF9hdCI6IjIwMjYtMDQtMDFUMDA6MDA6MDBaIiwidmVyaWZ5X3VybCI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC92ZXJpZnkvb3BfdHlwZWRfY2xhaW1zIn0sImV4dGVuZHMiOlsiZGV2LnVjcC5zaG9wcGluZy5jaGVja291dCIsImRldi51Y3Auc2hvcHBpbmcuY2FydCJdLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL2lkZW50aXR5IiwidmVyc2lvbiI6IjEifV19LCJuYW1lIjoiVHlwZWQgQ2xhaW1zIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6e30sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly90LmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.MzebNr-eOGPHe84z8ARhjFHmSLju7AvwgsSu4KY_tmg5R_T6xYrx7tZXbYOCfiSaZoFmpIJcXPakit4c-yxMAA" + }, + "jwks": { + "keys": [ + { + "kid": "node-typed-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "6JcesuEfiy104P6W8zOsruWkL7Ju7RLXMyR2F3fQ4xM" + } + ] + }, + "alg": "EdDSA", + "kid": "node-typed-claims-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-unicode.json b/tests/fixtures/cross-lang/node-unicode.json new file mode 100644 index 0000000..89fb48b --- /dev/null +++ b/tests/fixtures/cross-lang/node-unicode.json @@ -0,0 +1,59 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://日本.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "note": "メモ" + } + } + ] + }, + "name": "Café 日本 🍷 Merchant" + }, + "signing_keys": [ + { + "kid": "node-unicode-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "At1k1YXploco8YrjdagqC9HYxCnN7ommm4MWIRUp5AY" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJBdDFrMVlYcGxvY284WXJqZGFncUM5SFl4Q25ON29tbW00TVdJUlVwNUFZIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkNhZsOpIOaXpeacrCDwn423IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJub3RlIjoi44Oh44OiIn0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL-aXpeacrC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.BKhv1J0LSZ-PQBySoMQXTAx-OalhQZSiiCXaWSHjA6HbeCvz4aw-os3p-FlAgfDoiChxRKeGfN-n-LcYIv4yAQ" + }, + "jwks": { + "keys": [ + { + "kid": "node-unicode-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "At1k1YXploco8YrjdagqC9HYxCnN7ommm4MWIRUp5AY" + } + ] + }, + "alg": "EdDSA", + "kid": "node-unicode-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/py-capability.json b/tests/fixtures/cross-lang/py-capability.json new file mode 100644 index 0000000..af06dd8 --- /dev/null +++ b/tests/fixtures/cross-lang/py-capability.json @@ -0,0 +1,69 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://c.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "kyc_required": true + } + ] + }, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + } + ] + }, + "name": "Capability Merchant" + }, + "signing_keys": [ + { + "kid": "py-capability-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "TikhC4jSghoLfPC6j9KBytlHrgyFvZVVm5OUjG7bYCM" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiVGlraEM0alNnaG9MZlBDNmo5S0J5dGxIcmd5RnZaVlZtNU9Vakc3YllDTSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6eyJzaC5hZ2VudHNjb3JlLmlkZW50aXR5IjpbeyJreWNfcmVxdWlyZWQiOnRydWUsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vaWRlbnRpdHkiLCJ2ZXJzaW9uIjoiMSJ9XX0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJjaGFpbl9pZCI6NDIxNywicmFpbCI6InRlbXBvLW1haW5uZXQifSwiaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0._31-NgZEBmN2c8qyxQvOaEBrhycJ6MULjhfN3sgVp5UqiUduGp66XHQC0HI4Ni6W7CzNx2-ktZWdLWD0clPdDg" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "TikhC4jSghoLfPC6j9KBytlHrgyFvZVVm5OUjG7bYCM", + "kid": "py-capability-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-capability-EdDSA", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-data-driven-claims.json b/tests/fixtures/cross-lang/py-data-driven-claims.json new file mode 100644 index 0000000..5e3bb79 --- /dev/null +++ b/tests/fixtures/cross-lang/py-data-driven-claims.json @@ -0,0 +1,69 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://d.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ], + "claims": { + "operator_id": "op_data_driven", + "kyc_level": "none", + "sanctions_clear": false, + "age_bracket": "unknown", + "jurisdiction": "", + "verified_at": null, + "verify_url": "https://agentscore.sh/verify/op_data_driven", + "issuer": "https://agentscore.sh" + } + } + ] + }, + "payment_handlers": {}, + "name": "Data Driven Claims Merchant" + }, + "signing_keys": [ + { + "kid": "py-data-driven-claims-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "g_RzTBbrZ0krF4_f4Rtm__flo_1RH2sxiTF9dLltpC8" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJnX1J6VEJiclowa3JGNF9mNFJ0bV9fZmxvXzFSSDJzeGlURjlkTGx0cEM4In1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJleHRlbmRzIjpbImRldi51Y3Auc2hvcHBpbmcuY2hlY2tvdXQiLCJkZXYudWNwLnNob3BwaW5nLmNhcnQiXSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIxIn1dfSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vZC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.X7Xdu_60_sT2XpwD9SqF7Lpuf5OGlbG_t_sxaY1xf7rQID5fSR-4BEdB0Dppq04nuaedhcqGUeyTMZDfHe8YDQ" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "g_RzTBbrZ0krF4_f4Rtm__flo_1RH2sxiTF9dLltpC8", + "kid": "py-data-driven-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-data-driven-claims-EdDSA", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-emoji-keys.json b/tests/fixtures/cross-lang/py-emoji-keys.json new file mode 100644 index 0000000..209540d --- /dev/null +++ b/tests/fixtures/cross-lang/py-emoji-keys.json @@ -0,0 +1,60 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://emoji.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json" + } + ] + }, + "name": "Emoji Keys Merchant" + }, + "signing_keys": [ + { + "kid": "py-emoji-keys-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "t9o2BRiSJvI4c7a3KlzCqzKS1evXIyngTwB2GBxtZec" + } + ], + "a": 1, + "豈": 2, + "": 3, + "🍷": 4, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyIiOjMsImEiOjEsInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZW1vamkta2V5cy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJ0OW8yQlJpU0p2STRjN2EzS2x6Q3F6S1MxZXZYSXluZ1R3QjJHQnh0WmVjIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkVtb2ppIEtleXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vZW1vamkuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In0sIuixiCI6Miwi8J-NtyI6NH0.JFxAqyuCgvA0HNAl2giJeb4MbHDuW5h7jBjGcrQxcSDCiCgXjzhaUSWJXjiB7GeHcL7CDMg3kj79VQ4Rsr1-Bw" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "t9o2BRiSJvI4c7a3KlzCqzKS1evXIyngTwB2GBxtZec", + "kid": "py-emoji-keys-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-emoji-keys-EdDSA", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-es256-rails.json b/tests/fixtures/cross-lang/py-es256-rails.json new file mode 100644 index 0000000..4c9ffa9 --- /dev/null +++ b/tests/fixtures/cross-lang/py-es256-rails.json @@ -0,0 +1,81 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://a.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + }, + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "a2a", + "endpoint": "https://a.example.com/.well-known/agent-card.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + } + ], + "sh.agentscore.payment.x402": [ + { + "id": "x402", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/x402", + "schema": "https://agentscore.sh/schemas/payment-handlers/x402.json", + "config": { + "networks": [ + "base-8453" + ] + } + } + ] + }, + "name": "ES256 Merchant" + }, + "signing_keys": [ + { + "kid": "py-es256-rails-ES256", + "kty": "EC", + "alg": "ES256", + "use": "sig", + "crv": "P-256", + "x": "NFS5qrSPV5sDQ5hHVag2zFqOSpTO6NBL-Hqf9EjBOco", + "y": "wtbEwX6TxEFid1IJvIwxkVfNic3Q_xEOq7j54Kje7aY" + } + ], + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVTMjU2IiwiY3J2IjoiUC0yNTYiLCJraWQiOiJweS1lczI1Ni1yYWlscy1FUzI1NiIsImt0eSI6IkVDIiwidXNlIjoic2lnIiwieCI6Ik5GUzVxclNQVjVzRFE1aEhWYWcyekZxT1NwVE82TkJMLUhxZjlFakJPY28iLCJ5Ijoid3RiRXdYNlR4RUZpZDFJSnZJd3hrVmZOaWMzUV94RU9xN2o1NEtqZTdhWSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJFUzI1NiBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dLCJzaC5hZ2VudHNjb3JlLnBheW1lbnQueDQwMiI6W3siY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwiaWQiOiJ4NDAyIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy94NDAyLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy94NDAyIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In0seyJlbmRwb2ludCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS8ud2VsbC1rbm93bi9hZ2VudC1jYXJkLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6ImEyYSIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.Vo7XPWeW37oSI5Eub7oUVwb3ODY3g70PeNgYLODGQ9L2nrf-5K7yinG2QwHEh5GtIMq7fXp5fiQVk1KtFnL4Wg" + }, + "jwks": { + "keys": [ + { + "crv": "P-256", + "x": "NFS5qrSPV5sDQ5hHVag2zFqOSpTO6NBL-Hqf9EjBOco", + "y": "wtbEwX6TxEFid1IJvIwxkVfNic3Q_xEOq7j54Kje7aY", + "kid": "py-es256-rails-ES256", + "alg": "ES256", + "use": "sig", + "kty": "EC" + } + ] + }, + "alg": "ES256", + "kid": "py-es256-rails-ES256", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-extras-int.json b/tests/fixtures/cross-lang/py-extras-int.json new file mode 100644 index 0000000..f1380bd --- /dev/null +++ b/tests/fixtures/cross-lang/py-extras-int.json @@ -0,0 +1,60 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://e.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.stripe-spt": [ + { + "id": "stripe", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/stripe-spt", + "schema": "https://agentscore.sh/schemas/payment-handlers/stripe-spt.json", + "config": { + "profile_id": "abc", + "count": 7 + } + } + ] + }, + "name": "Extras Merchant" + }, + "signing_keys": [ + { + "kid": "py-extras-int-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "El2ke55St-sfq6gYs6wYJyJX7TIw3-spyA1hlMiNhpM" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiRWwya2U1NVN0LXNmcTZnWXM2d1lKeUpYN1RJdzMtc3B5QTFobE1pTmhwTSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJFeHRyYXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC5zdHJpcGUtc3B0IjpbeyJjb25maWciOnsiY291bnQiOjcsInByb2ZpbGVfaWQiOiJhYmMifSwiaWQiOiJzdHJpcGUiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3N0cmlwZS1zcHQuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3N0cmlwZS1zcHQiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9lLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.0DAtQpZ-9e8U3cmpzTHwWFZq2LmmchY6mz-rhRxybkNX4YDlqpPLcfAig7ybMzdo_O7afJ9QDNYfDERmCGtVDQ" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "El2ke55St-sfq6gYs6wYJyJX7TIw3-spyA1hlMiNhpM", + "kid": "py-extras-int-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-extras-int-EdDSA", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-int-boundary.json b/tests/fixtures/cross-lang/py-int-boundary.json new file mode 100644 index 0000000..9c91acc --- /dev/null +++ b/tests/fixtures/cross-lang/py-int-boundary.json @@ -0,0 +1,52 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://i.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": {}, + "name": "Int Boundary Merchant" + }, + "signing_keys": [ + { + "kid": "py-int-boundary-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "b5OlULxsP0xpS8IkLF4tRaiB1u6yODPxsQJJYv1iB6s" + } + ], + "max_safe_int": 9007199254740991, + "min_safe_int": -9007199254740991, + "small_int": 42, + "neg_small_int": -42, + "zero": 0, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5lZ19zbWFsbF9pbnQiOi00Miwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiYjVPbFVMeHNQMHhwUzhJa0xGNHRSYWlCMXU2eU9EUHhzUUpKWXYxaUI2cyJ9XSwic21hbGxfaW50Ijo0MiwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7fSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL2kuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In0sInplcm8iOjB9.PsM9i8EXGN5eNPJI6_6Efk8P-aE-gQQvmXpNCr1vTFMtsjvUrwPO974mweqhbyogrdfm47UkAhJZ2tkGQ26YDQ" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "b5OlULxsP0xpS8IkLF4tRaiB1u6yODPxsQJJYv1iB6s", + "kid": "py-int-boundary-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-int-boundary-EdDSA", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-minimal.json b/tests/fixtures/cross-lang/py-minimal.json new file mode 100644 index 0000000..0e83bfc --- /dev/null +++ b/tests/fixtures/cross-lang/py-minimal.json @@ -0,0 +1,47 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://m.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": {}, + "name": "Minimal Merchant" + }, + "signing_keys": [ + { + "kid": "py-minimal-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "dZ6PLK4BfgrHTuRA0klbkcl6iHAXhyX3ACjRefxb8IA" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiZFo2UExLNEJmZ3JIVHVSQTBrbGJrY2w2aUhBWGh5WDNBQ2pSZWZ4YjhJQSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJNaW5pbWFsIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6e30sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9tLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.axue3k1ojtSWw0pZJbuDmx-HBt6DZTwtbD3DiHKwrrP3YSWjdlp_FBfBMT0jA-oQ6HqfdQ4fO9vuRAAIpBepCw" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "dZ6PLK4BfgrHTuRA0klbkcl6iHAXhyX3ACjRefxb8IA", + "kid": "py-minimal-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-minimal-EdDSA", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-multikey.json b/tests/fixtures/cross-lang/py-multikey.json new file mode 100644 index 0000000..514eaba --- /dev/null +++ b/tests/fixtures/cross-lang/py-multikey.json @@ -0,0 +1,75 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://mk.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet" + } + } + ] + }, + "name": "Multi-Key Merchant" + }, + "signing_keys": [ + { + "kid": "py-multikey-old", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "lW7nqnsPzl7FVllMcMjTSHmAqaMVeBMJk4mEwgfY5Vo" + }, + { + "kid": "py-multikey-new", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "Kmwcte5hHWi17aQjekr9Zdw6fsBQl237_jllIAJBMnk" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJsVzducW5zUHpsN0ZWbGxNY01qVFNIbUFxYU1WZUJNSms0bUV3Z2ZZNVZvIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1uZXciLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiS213Y3RlNWhIV2kxN2FRamVrcjlaZHc2ZnNCUWwyMzdfamxsSUFKQk1uayJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJNdWx0aS1LZXkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL21rLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.gBimQYPBcvQFutbEzKeJrLzjrkgqyClkbRuSVOaRAfzAvUsxZ5Zse1WmqhadHzv5DUZohfBiWUHjj96kToOPDQ" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "lW7nqnsPzl7FVllMcMjTSHmAqaMVeBMJk4mEwgfY5Vo", + "kid": "py-multikey-old", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + }, + { + "crv": "Ed25519", + "x": "Kmwcte5hHWi17aQjekr9Zdw6fsBQl237_jllIAJBMnk", + "kid": "py-multikey-new", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-multikey-new", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-typed-claims.json b/tests/fixtures/cross-lang/py-typed-claims.json new file mode 100644 index 0000000..a486c17 --- /dev/null +++ b/tests/fixtures/cross-lang/py-typed-claims.json @@ -0,0 +1,69 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://t.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ], + "claims": { + "operator_id": "op_typed_claims", + "kyc_level": "enhanced", + "sanctions_clear": true, + "age_bracket": "21+", + "jurisdiction": "US", + "verified_at": "2026-04-01T00:00:00Z", + "verify_url": "https://agentscore.sh/verify/op_typed_claims", + "issuer": "https://agentscore.sh" + } + } + ] + }, + "payment_handlers": {}, + "name": "Typed Claims Merchant" + }, + "signing_keys": [ + { + "kid": "py-typed-claims-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "clSTIoRWvV4whYX40RYSSPGfcj2mL3YW-IkgYYM6SLQ" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJjbFNUSW9SV3ZWNHdoWVg0MFJZU1NQR2ZjajJtTDNZVy1Ja2dZWU02U0xRIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJleHRlbmRzIjpbImRldi51Y3Auc2hvcHBpbmcuY2hlY2tvdXQiLCJkZXYudWNwLnNob3BwaW5nLmNhcnQiXSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIxIn1dfSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vdC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.0BQic1wyTNOk4TVcs2dJ6iRARokGtSjzMzbP9myAlMdF8zpnXfWAzZ6MwUsmH10eK7PQtRrj5D-St4_xxw6SBw" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "clSTIoRWvV4whYX40RYSSPGfcj2mL3YW-IkgYYM6SLQ", + "kid": "py-typed-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-typed-claims-EdDSA", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-unicode.json b/tests/fixtures/cross-lang/py-unicode.json new file mode 100644 index 0000000..8d27413 --- /dev/null +++ b/tests/fixtures/cross-lang/py-unicode.json @@ -0,0 +1,59 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://日本.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "note": "メモ" + } + } + ] + }, + "name": "Café 日本 🍷 Merchant" + }, + "signing_keys": [ + { + "kid": "py-unicode-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "Rk_x9yyAht9Xy_mKxxmdh0kyr12andlLUGHY2xh8-3w" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiUmtfeDl5eUFodDlYeV9tS3h4bWRoMGt5cjEyYW5kbExVR0hZMnhoOC0zdyJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJDYWbDqSDml6XmnKwg8J-NtyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly_ml6XmnKwuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In19.fraS8Y7ecHdldvmqwIdCzvSlBqi2GvYatX4UmSnR0jBnKDY8qxQnfYErAbJQ8ywXnP8Ztsp7PvbaRd90GIZ0CQ" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "Rk_x9yyAht9Xy_mKxxmdh0kyr12andlLUGHY2xh8-3w", + "kid": "py-unicode-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-unicode-EdDSA", + "generator": "python" +} diff --git a/tests/test_a2a.py b/tests/test_a2a.py index 73398f3..fdebc97 100644 --- a/tests/test_a2a.py +++ b/tests/test_a2a.py @@ -1,9 +1,11 @@ """Tests for build_a2a_agent_card.""" from agentscore_commerce.identity import ( + UCP_A2A_EXTENSION_URI, A2AAgentCardCapabilities, AssessResult, build_a2a_agent_card, + ucp_a2a_extension, ) @@ -100,3 +102,52 @@ def test_respects_issuer_and_verify_url_overrides(): assert card.identity is not None assert card.identity.issuer == "https://other.example" assert card.identity.verify_url == "https://other.example/v" + + +def test_ucp_a2a_extension_uri_pinned_to_2026_04_08(): + assert UCP_A2A_EXTENSION_URI == "https://ucp.dev/2026-04-08/specification/reference" + + +def test_ucp_a2a_extension_no_args_produces_empty_capabilities_entry(): + ext = ucp_a2a_extension() + assert ext.uri == UCP_A2A_EXTENSION_URI + assert ext.params == {"capabilities": {}} + + +def test_ucp_a2a_extension_wraps_capabilities_map_under_params(): + ext = ucp_a2a_extension( + { + "dev.ucp.shopping.checkout": [{"version": "2026-04-08"}], + "dev.ucp.shopping.cart": [{"version": "2026-04-08"}], + }, + ) + assert ext.params == { + "capabilities": { + "dev.ucp.shopping.checkout": [{"version": "2026-04-08"}], + "dev.ucp.shopping.cart": [{"version": "2026-04-08"}], + }, + } + + +def test_build_a2a_agent_card_emits_extensions_when_passed(): + card = build_a2a_agent_card( + name="X", + extensions=[ucp_a2a_extension()], + ) + d = card.to_dict() + assert "extensions" in d + assert len(d["extensions"]) == 1 + assert d["extensions"][0]["uri"] == UCP_A2A_EXTENSION_URI + assert d["extensions"][0]["params"] == {"capabilities": {}} + + +def test_build_a2a_agent_card_omits_extensions_when_not_passed(): + card = build_a2a_agent_card(name="X") + assert "extensions" not in card.to_dict() + + +def test_build_a2a_agent_card_omits_extensions_when_passed_empty_list(): + # Parity with node: both SDKs skip the extensions field when empty so + # cross-language profiles canonicalize to identical bytes when both omit. + card = build_a2a_agent_card(name="X", extensions=[]) + assert "extensions" not in card.to_dict() diff --git a/tests/test_ucp.py b/tests/test_ucp.py index 493e198..95be163 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -1,11 +1,17 @@ -"""Tests for build_ucp_profile.""" +"""Tests for build_ucp_profile (spec-compliant shape).""" + +from typing import cast + +import pytest from agentscore_commerce.identity import ( AGENTSCORE_UCP_CAPABILITY, AssessResult, - UCPCapability, - UCPPaymentHandler, - UCPService, + OperatorVerification, + UCPCapabilityBinding, + UCPPaymentHandlerBinding, + UCPProfileBody, + UCPServiceBinding, UCPSigningKey, build_ucp_profile, ) @@ -28,34 +34,53 @@ def _full_result() -> AssessResult: ) +def _sample_service() -> UCPServiceBinding: + return UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/2026-04-08/specification/overview", + transport="mcp", + endpoint="https://agents.example/api/ucp/mcp", + schema="https://ucp.dev/services/shopping/openrpc.json", + ) + + def _base_kwargs(): return { - "services": [UCPService(type="rest", url="https://agents.example")], + "services": {"dev.ucp.shopping": [_sample_service()]}, "signing_keys": [ UCPSigningKey(kid="me", kty="EC", alg="ES256", crv="P-256", extras={"x": "x", "y": "y"}), ], } -def test_base_profile_has_required_fields(): +def _agentscore_cap(d: dict) -> dict: + return d["ucp"]["capabilities"][AGENTSCORE_UCP_CAPABILITY][0] + + +def test_emits_spec_envelope_with_ucp_body_and_outer_signing_keys(): profile = build_ucp_profile(**_base_kwargs()) d = profile.to_dict() - assert d["spec"] == "https://ucp.dev/" - assert "version" in d - assert d["services"][0]["url"] == "https://agents.example" + assert "ucp" in d + assert "signing_keys" in d + # No top-level `spec` field per UCP spec — spec lives per-binding. + assert "spec" not in d + assert "version" not in d # version lives under `ucp` + assert d["ucp"]["version"] + assert d["ucp"]["services"]["dev.ucp.shopping"][0]["transport"] == "mcp" + assert d["ucp"]["capabilities"] == {} + assert d["ucp"]["payment_handlers"] == {} assert d["signing_keys"][0]["kid"] == "me" - assert d["capabilities"] == [] - assert d["payment_handlers"] == [] def test_appends_agentscore_capability_when_data_provided(): profile = build_ucp_profile(**_base_kwargs(), data=_full_result()) d = profile.to_dict() - matching = [c for c in d["capabilities"] if c["name"] == AGENTSCORE_UCP_CAPABILITY] - assert len(matching) == 1 - cap = matching[0] + cap = _agentscore_cap(d) assert cap["version"] == "1" - assert "agentscore-identity.v1.json" in cap["schema"] + assert "sh-agentscore-identity-v1.json" in cap["schema"] + # Multi-parent extends — matches Shopify's dev.shopify.catalog.storefront pattern + # and UCP-canonical dev.ucp.shopping.discount (extends [checkout, cart]). + assert cap["extends"] == ["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"] claims = cap["claims"] assert claims["operator_id"] == "op_abc" assert claims["kyc_level"] == "enhanced" @@ -66,39 +91,66 @@ def test_appends_agentscore_capability_when_data_provided(): def test_skips_agentscore_capability_when_no_resolved_operator(): profile = build_ucp_profile(**_base_kwargs(), data=AssessResult(allow=True, resolved_operator=None)) d = profile.to_dict() - assert all(c["name"] != AGENTSCORE_UCP_CAPABILITY for c in d["capabilities"]) + assert AGENTSCORE_UCP_CAPABILITY not in d["ucp"]["capabilities"] def test_preserves_caller_capabilities_and_appends_agentscore(): + checkout_binding = UCPCapabilityBinding( + version="2026-04-08", + spec="https://ucp.dev/2026-04-08/specification/checkout", + schema="https://ucp.dev/2026-04-08/schemas/shopping/checkout.json", + ) profile = build_ucp_profile( **_base_kwargs(), - capabilities=[UCPCapability(name="checkout", version="2")], + capabilities={"dev.ucp.shopping.checkout": [checkout_binding]}, data=_full_result(), ) d = profile.to_dict() - assert d["capabilities"][0]["name"] == "checkout" - assert d["capabilities"][1]["name"] == AGENTSCORE_UCP_CAPABILITY + assert d["ucp"]["capabilities"]["dev.ucp.shopping.checkout"][0]["version"] == "2026-04-08" + assert _agentscore_cap(d)["version"] == "1" def test_passes_through_name_payment_handlers_extras(): + tempo_handler = UCPPaymentHandlerBinding( + id="tempo", + version="2026-04-08", + spec="https://agentscore.sh/specification/payment-handlers/tempo", + schema="https://agentscore.sh/schemas/payment-handlers/tempo.json", + config={"recipient": "0xtempo"}, + ) profile = build_ucp_profile( **_base_kwargs(), name="Example Merchant", - payment_handlers=[ - UCPPaymentHandler(name="tempo", config={"recipient": "0xtempo"}), - UCPPaymentHandler(name="stripe", config={"profile_id": "prof_x"}), - ], - extras={"custom_field": "custom_value"}, + payment_handlers={"sh.agentscore.payment.tempo": [tempo_handler]}, + extras={"custom_top_level": "top_value"}, + ucp_extras={"custom_ucp_field": "ucp_value"}, ) d = profile.to_dict() - assert d["name"] == "Example Merchant" - assert len(d["payment_handlers"]) == 2 - assert d["custom_field"] == "custom_value" + assert d["ucp"]["name"] == "Example Merchant" + assert d["ucp"]["payment_handlers"]["sh.agentscore.payment.tempo"][0]["id"] == "tempo" + assert d["custom_top_level"] == "top_value" + assert d["ucp"]["custom_ucp_field"] == "ucp_value" + + +def test_payment_handler_omits_config_when_caller_does_not_set_it(): + handler = UCPPaymentHandlerBinding( + id="tempo", + version="2026-04-08", + spec="https://agentscore.sh/specification/payment-handlers/tempo", + schema="https://agentscore.sh/schemas/payment-handlers/tempo.json", + ) + profile = build_ucp_profile( + **_base_kwargs(), + payment_handlers={"sh.agentscore.payment.tempo": [handler]}, + ) + d = profile.to_dict() + serialized = d["ucp"]["payment_handlers"]["sh.agentscore.payment.tempo"][0] + assert "config" not in serialized def test_respects_version_override(): profile = build_ucp_profile(**_base_kwargs(), version="2026-12-31") - assert profile.version == "2026-12-31" + assert profile.ucp.version == "2026-12-31" def test_respects_agentscore_schema_url_override(): @@ -107,5 +159,321 @@ def test_respects_agentscore_schema_url_override(): data=_full_result(), agentscore_schema_url="https://custom.example/schema.json", ) - cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] assert cap.schema == "https://custom.example/schema.json" + + +def test_respects_agentscore_spec_url_override(): + profile = build_ucp_profile( + **_base_kwargs(), + data=_full_result(), + agentscore_spec_url="https://custom.example/spec", + ) + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] + assert cap.spec == "https://custom.example/spec" + + +def test_emits_supported_versions_map_when_supplied(): + profile = build_ucp_profile( + **_base_kwargs(), + supported_versions={ + "2026-04-08": "https://merchant.example/.well-known/ucp/2026-04-08", + "2026-01-23": "https://merchant.example/.well-known/ucp/2026-01-23", + }, + ) + d = profile.to_dict() + assert d["ucp"]["supported_versions"]["2026-04-08"].endswith("/2026-04-08") + + +@pytest.mark.parametrize( + "key", + ["ucp", "signing_keys", "signature", "__proto__", "constructor", "prototype"], +) +def test_extras_top_level_reserved_collision_rejected(key: str) -> None: + profile = build_ucp_profile(**_base_kwargs(), extras={key: "attacker"}) + with pytest.raises(ValueError, match="collides with a reserved profile field"): + profile.to_dict() + + +@pytest.mark.parametrize( + "key", + [ + "version", + "name", + "services", + "capabilities", + "payment_handlers", + "supported_versions", + "__proto__", + "constructor", + "prototype", + ], +) +def test_ucp_extras_reserved_collision_rejected(key: str) -> None: + profile = build_ucp_profile(**_base_kwargs(), ucp_extras={key: "attacker"}) + with pytest.raises(ValueError, match="collides with a reserved `ucp` field"): + profile.to_dict() + + +# Empty-string and null normalization: the API can emit `account_verification` with +# either null or "" for un-set fields, and the node + python siblings must produce +# the SAME canonical claims block for either shape so a profile signed in one +# language verifies in the other. + + +def _claims_of(account_verification: dict, operator_verification: dict | None = None) -> dict: + raw: dict = {"account_verification": account_verification} + if operator_verification is not None: + raw["operator_verification"] = operator_verification + result = AssessResult(allow=True, resolved_operator="op_abc", raw=raw) + profile = build_ucp_profile(**_base_kwargs(), data=result) + d = profile.to_dict() + return _agentscore_cap(d)["claims"] + + +def test_coerces_empty_string_kyc_level_to_none() -> None: + assert _claims_of({"kyc_level": ""})["kyc_level"] == "none" + + +def test_coerces_null_age_bracket_to_unknown() -> None: + assert _claims_of({"age_bracket": None})["age_bracket"] == "unknown" + + +def test_coerces_empty_string_age_bracket_to_unknown() -> None: + assert _claims_of({"age_bracket": ""})["age_bracket"] == "unknown" + + +def test_coerces_null_jurisdiction_to_empty_string() -> None: + assert _claims_of({"jurisdiction": None})["jurisdiction"] == "" + + +def test_coerces_empty_string_jurisdiction_to_empty_string() -> None: + assert _claims_of({"jurisdiction": ""})["jurisdiction"] == "" + + +def test_coerces_null_verified_at_to_none() -> None: + assert _claims_of({"verified_at": None})["verified_at"] is None + + +def test_coerces_empty_string_verified_at_to_none() -> None: + assert _claims_of({"verified_at": ""})["verified_at"] is None + + +def test_both_empty_string_verified_at_normalizes_to_none() -> None: + """Both account_verification + operator_verification with verified_at="" + must normalize to None for cross-language byte parity with Node SDK. + """ + assert ( + _claims_of( + {"verified_at": ""}, + operator_verification={"verified_at": ""}, + )["verified_at"] + is None + ) + + +# Typed-field fallback: production callers populate `data.raw`, but a +# hand-constructed AssessResult (no raw) should still surface the verification +# block via the typed `AssessResult.operator_verification` / +# `AssessResult.account_verification` fields. Mirrors the node sibling's +# typed-field read path. + + +def test_typed_operator_verification_fallback_when_raw_is_none() -> None: + result = AssessResult( + allow=True, + resolved_operator="op_typed", + operator_verification=OperatorVerification( + level="enhanced", + operator_type="api", + verified_at="2026-04-01T00:00:00Z", + ), + raw=None, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + d = profile.to_dict() + claims = _agentscore_cap(d)["claims"] + assert claims["operator_id"] == "op_typed" + assert claims["kyc_level"] == "enhanced" + assert claims["verified_at"] == "2026-04-01T00:00:00Z" + + +def test_typed_account_verification_fallback_when_raw_is_none() -> None: + result = AssessResult( + allow=True, + resolved_operator="op_typed", + operator_verification=OperatorVerification(level="verified"), + account_verification={ + "kyc_level": "verified", + "age_bracket": "21+", + "jurisdiction": "US", + "sanctions_clear": True, + }, + raw=None, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + d = profile.to_dict() + claims = _agentscore_cap(d)["claims"] + assert claims["kyc_level"] == "verified" + assert claims["age_bracket"] == "21+" + assert claims["jurisdiction"] == "US" + assert claims["sanctions_clear"] is True + + +def test_typed_takes_precedence_over_raw() -> None: + result = AssessResult( + allow=True, + resolved_operator="op_xyz", + operator_verification=OperatorVerification(level="verified"), + account_verification={"kyc_level": "verified"}, + raw={ + "operator_verification": {"level": "none"}, + "account_verification": {"kyc_level": "none"}, + }, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] + assert cap.extras["claims"]["kyc_level"] == "verified" + + +def test_raw_fallback_used_when_typed_missing() -> None: + result = AssessResult( + allow=True, + resolved_operator="op_raw", + operator_verification=None, + raw={ + "operator_verification": {"level": "enhanced"}, + "account_verification": {"kyc_level": "enhanced"}, + }, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] + assert cap.extras["claims"]["kyc_level"] == "enhanced" + + +# Per-element to_dict reserved-key collision guard. Vendor extras can't silently +# overwrite a canonical field on the new binding dataclasses. + + +def test_ucp_service_binding_extras_collision_rejected() -> None: + svc = UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/spec", + transport="rest", + extras={"transport": "different"}, + ) + with pytest.raises(ValueError, match=r"UCPServiceBinding\.extras key 'transport' collides"): + svc.to_dict() + + +def test_ucp_service_binding_extras_non_reserved_pass_through() -> None: + svc = UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/spec", + transport="rest", + endpoint="https://x.example", + extras={"region": "us-west-1"}, + ) + out = svc.to_dict() + assert out["region"] == "us-west-1" + assert out["endpoint"] == "https://x.example" + + +def test_ucp_capability_binding_extras_collision_rejected() -> None: + cap = UCPCapabilityBinding( + version="1", + spec="https://x/spec", + schema="https://x/schema", + extras={"schema": "https://attacker"}, + ) + with pytest.raises(ValueError, match=r"UCPCapabilityBinding\.extras key 'schema' collides"): + cap.to_dict() + + +def test_ucp_capability_binding_claims_extra_passes_through() -> None: + cap = UCPCapabilityBinding( + version="1", + spec="https://x/spec", + schema="https://x/schema", + extras={"claims": {"k": "v"}}, + ) + out = cap.to_dict() + assert out["claims"] == {"k": "v"} + + +def test_ucp_payment_handler_binding_omits_default_empty_config() -> None: + h = UCPPaymentHandlerBinding( + id="tempo", + version="1", + spec="https://x", + schema="https://x", + ) + out = h.to_dict() + assert "config" not in out + assert out["id"] == "tempo" + + +def test_ucp_payment_handler_binding_omits_explicit_empty_config() -> None: + h = UCPPaymentHandlerBinding( + id="tempo", + version="1", + spec="https://x", + schema="https://x", + config={}, + ) + assert "config" not in h.to_dict() + + +def test_ucp_payment_handler_binding_preserves_populated_config() -> None: + h = UCPPaymentHandlerBinding( + id="tempo", + version="1", + spec="https://x", + schema="https://x", + config={"recipient": "0xabc"}, + ) + out = h.to_dict() + assert out["config"] == {"recipient": "0xabc"} + + +def test_ucp_signing_key_extras_collision_with_kid_rejected() -> None: + sk = UCPSigningKey(kid="me", kty="EC", extras={"kid": "attacker"}) + with pytest.raises(ValueError, match=r"UCPSigningKey\.extras key 'kid' collides"): + sk.to_dict() + + +def test_ucp_signing_key_extras_non_reserved_pass_through() -> None: + sk = UCPSigningKey(kid="me", kty="EC", alg="ES256", crv="P-256", extras={"x": "abc", "y": "def"}) + out = sk.to_dict() + assert out == {"kid": "me", "kty": "EC", "alg": "ES256", "crv": "P-256", "x": "abc", "y": "def"} + + +def test_typed_empty_account_verification_wins_over_raw() -> None: + result = AssessResult( + allow=True, + resolved_operator="op_xyz", + account_verification={}, + raw={"account_verification": {"kyc_level": "verified"}}, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] + assert cap.extras["claims"]["kyc_level"] == "none" + + +def test_typed_empty_operator_verification_wins_over_raw() -> None: + result = AssessResult( + allow=True, + resolved_operator="op_xyz", + operator_verification=cast("OperatorVerification", {}), + raw={"operator_verification": {"level": "enhanced", "verified_at": "2026-01-01T00:00:00Z"}}, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] + assert cap.extras["claims"]["verified_at"] is None + + +def test_ucp_profile_body_can_be_constructed_directly() -> None: + """UCPProfileBody is exported so callers can pre-build the body if they want.""" + body = UCPProfileBody(version="2026-04-08") + assert body.to_dict()["version"] == "2026-04-08" + assert body.to_dict()["services"] == {} diff --git a/tests/test_ucp_cross_lang.py b/tests/test_ucp_cross_lang.py new file mode 100644 index 0000000..2a737b1 --- /dev/null +++ b/tests/test_ucp_cross_lang.py @@ -0,0 +1,60 @@ +"""Cross-language UCP signing fixture corpus. + +Each fixture file is a ``{profile, jwks, alg, kid, generator}`` envelope. Both +Node and Python check in identical fixtures so a future canonicalization change +in either language fails CI loudly. Without this, cross-language byte parity +drift would silently break verifier-side compatibility in production. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from agentscore_commerce.identity import verify_ucp_profile + +FIXTURE_DIR = Path(__file__).parent / "fixtures" / "cross-lang" +FIXTURES = sorted(FIXTURE_DIR.glob("*.json")) + + +@pytest.mark.parametrize("fixture_path", FIXTURES, ids=[p.name for p in FIXTURES]) +def test_verifies_cross_lang_fixture(fixture_path: Path) -> None: + data = json.loads(fixture_path.read_text()) + assert verify_ucp_profile(data["profile"], data["jwks"]) is True + + +def test_corpus_covers_canonical_scenarios() -> None: + names = {p.name for p in FIXTURES} + generators = {json.loads(p.read_text())["generator"] for p in FIXTURES} + assert "node" in generators + assert "python" in generators + # `emoji-keys` exercises non-ASCII object keys with codepoints that genuinely + # distinguish UTF-16 first-unit sort from Unicode codepoint sort: BMP private use + # (U+E000) ranks BEFORE supplementary plane (U+1F377) by codepoint but AFTER it by + # UTF-16 first unit (because the high surrogate 55356 < 57344). Both repos ship the + # node and python emoji-keys fixtures so a regression in either language's key sort + # surfaces here. + for lang in ("node", "py"): + for scenario in ( + "minimal", + "es256-rails", + "extras-int", + "capability", + "unicode", + "multikey", + "emoji-keys", + "int-boundary", + # `data-driven-claims` exercises the raw-dict fallback read path + # (`AssessResult(raw={"account_verification": {...}})`) that + # production callers populate. `typed-claims` exercises the typed + # field path (`AssessResult(account_verification={...}, raw=None)`) + # that hand-constructed callers use — Node's `buildUCPProfile` + # reads typed fields directly without consulting raw, so both + # paths must produce byte-identical canonical bytes across + # languages or cross-lang verify silently drifts. + "data-driven-claims", + "typed-claims", + ): + assert f"{lang}-{scenario}.json" in names, f"missing fixture {lang}-{scenario}.json" diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py new file mode 100644 index 0000000..2d28589 --- /dev/null +++ b/tests/test_ucp_jwks.py @@ -0,0 +1,843 @@ +"""Tests for UCP profile signing helpers (cross-language parity with node-commerce).""" + +from __future__ import annotations + +import pytest + +from agentscore_commerce.identity.ucp import UCPSigningKey +from agentscore_commerce.identity.ucp_jwks import ( + GeneratedUCPKey, + UCPVerificationError, + build_jwks_response, + generate_ucp_signing_key, + sign_ucp_profile, + verify_ucp_profile, +) + + +def _base_profile(signing_keys: list[dict]) -> dict: + return { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "name": "Test Merchant", + "services": [{"type": "rest", "url": "https://agents.example.com"}], + "capabilities": [], + "payment_handlers": [{"name": "tempo", "config": {"recipient": "0x1234"}}], + "signing_keys": signing_keys, + } + + +class TestGenerateUCPSigningKey: + def test_generates_eddsa_keypair_by_default(self) -> None: + key = generate_ucp_signing_key(kid="test-key-1") + assert key.private_key is not None + assert key.public_jwk["kid"] == "test-key-1" + assert key.public_jwk["alg"] == "EdDSA" + assert key.public_jwk["use"] == "sig" + assert key.public_jwk["kty"] == "OKP" + assert key.public_jwk["crv"] == "Ed25519" + assert isinstance(key.public_jwk.get("x"), str) + # private parts must NOT be in the exported public JWK + assert "d" not in key.public_jwk + + def test_generates_es256_keypair(self) -> None: + key = generate_ucp_signing_key(kid="es256-key", alg="ES256") + assert key.public_jwk["alg"] == "ES256" + assert key.public_jwk["kty"] == "EC" + assert key.public_jwk["crv"] == "P-256" + assert isinstance(key.public_jwk.get("x"), str) + assert isinstance(key.public_jwk.get("y"), str) + assert "d" not in key.public_jwk + + def test_distinct_kid_and_material(self) -> None: + a = generate_ucp_signing_key(kid="a") + b = generate_ucp_signing_key(kid="b") + assert a.public_jwk["kid"] == "a" + assert b.public_jwk["kid"] == "b" + assert a.public_jwk["x"] != b.public_jwk["x"] + + def test_unsupported_alg_raises(self) -> None: + with pytest.raises(ValueError, match="Unsupported UCP signing algorithm"): + generate_ucp_signing_key(kid="bad", alg="RS256") # type: ignore[arg-type] + + +class TestSignAndVerifyRoundTrip: + def test_eddsa_sign_verify_round_trip(self) -> None: + key = generate_ucp_signing_key(kid="merchant-2026-05") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="merchant-2026-05") + + assert "signature" in signed + assert isinstance(signed["signature"], str) + # JWS Compact has 3 segments separated by dots + assert len(signed["signature"].split(".")) == 3 + + ok = verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert ok is True + + def test_es256_sign_verify_round_trip(self) -> None: + key = generate_ucp_signing_key(kid="es256-key", alg="ES256") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="es256-key", alg="ES256") + ok = verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert ok is True + + def test_multi_key_jwks_resolves_by_kid(self) -> None: + old_key = generate_ucp_signing_key(kid="old-key") + new_key = generate_ucp_signing_key(kid="new-key") + profile = _base_profile([old_key.public_jwk, new_key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=new_key.private_key, kid="new-key") + ok = verify_ucp_profile(signed, build_jwks_response([old_key.public_jwk, new_key.public_jwk])) + assert ok is True + + def test_rejects_tampered_profile_body(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + + tampered = {**signed, "name": "Different Name"} + with pytest.raises(ValueError, match="does not match the signed payload"): + verify_ucp_profile(tampered, build_jwks_response([key.public_jwk])) + + def test_rejects_when_jwks_missing_signing_key(self) -> None: + signer = generate_ucp_signing_key(kid="signer") + other = generate_ucp_signing_key(kid="other") + profile = _base_profile([signer.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="signer") + + with pytest.raises(UCPVerificationError) as exc_info: + verify_ucp_profile(signed, build_jwks_response([other.public_jwk])) + assert exc_info.value.code == "kid_not_found" + + def test_rejects_profile_without_signature(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + with pytest.raises(ValueError, match="no `signature` field"): + verify_ucp_profile(profile, build_jwks_response([key.public_jwk])) + + +class TestCanonicalization: + def test_key_order_in_json_does_not_affect_verification(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + + # Hand-construct the same profile with keys in REVERSE insertion order + # so canonicalization actually has work to do. ``json.loads(json.dumps(...))`` + # preserves the source order on Python 3.7+, which is a vacuous round-trip. + reordered = {k: signed[k] for k in sorted(signed.keys(), reverse=True)} + assert next(iter(reordered)) != next(iter(sorted(signed))) # sanity + ok = verify_ucp_profile(reordered, build_jwks_response([key.public_jwk])) + assert ok is True + + +class TestBuildJWKSResponse: + def test_wraps_keys_in_keys_array(self) -> None: + k1 = {"kid": "a", "kty": "OKP", "crv": "Ed25519", "x": "xxx", "use": "sig", "alg": "EdDSA"} + k2 = {"kid": "b", "kty": "EC", "crv": "P-256", "x": "xxx", "y": "yyy", "use": "sig", "alg": "ES256"} + jwks = build_jwks_response([k1, k2]) + assert jwks == {"keys": [k1, k2]} + + def test_handles_empty_key_set(self) -> None: + assert build_jwks_response([]) == {"keys": []} + + +class TestSecurity: + """Coverage for alg-confusion + kid + typ + dup-kid + tampering attacks.""" + + def _hand_sign_compact(self, header: dict, payload_bytes: bytes, key: object, registry: object) -> str: + from joserfc import jws + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + # Cast for ty + reg = registry if isinstance(registry, JWSRegistry) else JWSRegistry(algorithms=["EdDSA", "ES256", "HS256"]) + return jws.serialize_compact(header, payload_bytes, key, registry=reg) + + def test_rejects_kid_less_jws(self) -> None: + """A JWS with no kid header is rejected even if the JWKS has a key that would verify.""" + from joserfc import jws + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + signer = generate_ucp_signing_key(kid="real-kid") + profile = _base_profile([signer.public_jwk]) + # Hand-craft a JWS with NO kid in the header. + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + registry = JWSRegistry(algorithms=["EdDSA", "ES256"]) + kid_less_sig = jws.serialize_compact( + {"alg": "EdDSA", "typ": "agentscore-profile+jws"}, + canonical, + signer.private_key, + registry=registry, + ) + signed = {**profile, "signature": kid_less_sig} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) + assert exc.value.code == "missing_kid" + + def test_rejects_wrong_typ(self) -> None: + from joserfc import jws + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + registry = JWSRegistry(algorithms=["EdDSA"]) + wrong_typ_sig = jws.serialize_compact( + {"alg": "EdDSA", "kid": "k", "typ": "JWT"}, + canonical, + signer.private_key, + registry=registry, + ) + signed = {**profile, "signature": wrong_typ_sig} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) + assert exc.value.code == "wrong_typ" + + def test_rejects_unsupported_alg(self) -> None: + from joserfc import jws + from joserfc.jwk import OctKey # type: ignore[import-not-found] + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + # Build a hostile oct key + HS256 sig over the canonical body of a real profile. + signer = generate_ucp_signing_key(kid="real") + profile = _base_profile([signer.public_jwk]) + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + oct_key = OctKey.generate_key(parameters={"kid": "real", "alg": "HS256", "use": "sig"}) + registry = JWSRegistry(algorithms=["HS256"]) + evil_sig = jws.serialize_compact( + {"alg": "HS256", "kid": "real", "typ": "agentscore-profile+jws"}, + canonical, + oct_key, + registry=registry, + ) + signed = {**profile, "signature": evil_sig} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) + assert exc.value.code == "unsupported_alg" + + def test_rejects_duplicate_kid_in_jwks(self) -> None: + a = generate_ucp_signing_key(kid="dup") + b = generate_ucp_signing_key(kid="dup") + profile = _base_profile([a.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=a.private_key, kid="dup") + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([a.public_jwk, b.public_jwk])) + assert exc.value.code == "duplicate_kid" + + def test_emits_typed_error_for_body_mismatch(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + tampered = {**signed, "name": "Different"} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, build_jwks_response([signer.public_jwk])) + assert exc.value.code == "body_mismatch" + + def test_emits_typed_error_for_no_signature(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(profile, build_jwks_response([signer.public_jwk])) + assert exc.value.code == "no_signature" + + def test_rejects_tampered_signature_segment(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + # Flip last char of the signature segment. + h, p, s = signed["signature"].split(".") + flipped_s = s[:-1] + ("B" if s.endswith("A") else "A") + tampered = {**signed, "signature": f"{h}.{p}.{flipped_s}"} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, build_jwks_response([signer.public_jwk])) + # joserfc may classify as either signature_invalid or malformed_jws depending on the flip. + assert exc.value.code in ("signature_invalid", "malformed_jws") + + def test_rejects_malformed_jws(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + garbage = {**profile, "signature": "not.a.jws"} + with pytest.raises(UCPVerificationError): + verify_ucp_profile(garbage, build_jwks_response([signer.public_jwk])) + + def test_eddsa_signing_is_deterministic(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + a = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + b = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert a["signature"] == b["signature"] + + def test_es256_signing_is_non_deterministic_but_both_verify(self) -> None: + signer = generate_ucp_signing_key(kid="k", alg="ES256") + profile = _base_profile([signer.public_jwk]) + a = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k", alg="ES256") + b = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k", alg="ES256") + assert a["signature"] != b["signature"] + assert verify_ucp_profile(a, build_jwks_response([signer.public_jwk])) is True + assert verify_ucp_profile(b, build_jwks_response([signer.public_jwk])) is True + + +class TestUnsafeNumberRejection: + def test_rejects_float_in_profile(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"rate": 0.0125}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_nan(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"value": float("nan")}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_positive_infinity(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"value": float("inf")}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_negative_infinity(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"value": float("-inf")}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_accepts_int_and_string(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"count": 7, "label": "wine"}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + def test_rejects_set_values_outright(self) -> None: + # `set` is not representable in JSON; the canonicalizer rejects it with a + # typed message before any element-level checks run. Mirrors node's + # `stableStringify: Set values are not allowed`. + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"vals": {0.5}}} + with pytest.raises(ValueError, match="set values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_frozenset_values_outright(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"vals": frozenset({0.25})}} + with pytest.raises(ValueError, match="frozenset values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_empty_set_with_typed_message(self) -> None: + # Empty set + set-of-valid-strings would fall through `_reject_unsafe_numbers` + # cleanly and surface a raw `TypeError` from `json.dumps` later. The typed + # reject ensures callers get a guiding ValueError instead. + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"vals": set()}} + with pytest.raises(ValueError, match="set values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_set_of_valid_strings_with_typed_message(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"vals": {"valid", "strings"}}} + with pytest.raises(ValueError, match="set values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_bytes_values_outright(self) -> None: + # `bytes` is not representable in JSON; the canonicalizer rejects it with a + # typed message before `json.dumps` can raise its raw + # `TypeError: Object of type bytes is not JSON serializable`. Mirrors + # node's `stableStringify: typed arrays are not allowed`. + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"blob": b"hello"}} + with pytest.raises(ValueError, match="bytes values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_bytearray_values_outright(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"blob": bytearray(b"hello")}} + with pytest.raises(ValueError, match="bytearray values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_empty_bytes_with_typed_message(self) -> None: + # Empty bytes would fall through `_reject_unsafe_numbers` cleanly and + # surface a raw `TypeError` from `json.dumps` later. The typed reject + # ensures callers get a guiding ValueError instead. + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"blob": b""}} + with pytest.raises(ValueError, match="bytes values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_accepts_max_safe_int_boundary(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"big": 2**53 - 1}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + def test_accepts_min_safe_int_boundary(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"big": -(2**53 - 1)}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + def test_rejects_int_above_max_safe_boundary(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"big": 2**53}} + with pytest.raises(ValueError, match="MAX_SAFE_INTEGER"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_int_well_above_max_safe(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"big": 2**60}} + with pytest.raises(ValueError, match="MAX_SAFE_INTEGER"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_int_below_min_safe(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"neg": -(2**53)}} + with pytest.raises(ValueError, match="MAX_SAFE_INTEGER"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_oversized_int_in_nested_list(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"a": [{"b": 2**60}]}} + with pytest.raises(ValueError, match="MAX_SAFE_INTEGER"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_accepts_bool_values(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"flag": True, "other": False}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + +class TestUCPSigningKeyFromJWK: + def test_round_trip_eddsa(self) -> None: + gen = generate_ucp_signing_key(kid="merchant-2026-05") + sk = UCPSigningKey.from_jwk(gen.public_jwk) + assert sk.kid == "merchant-2026-05" + assert sk.kty == "OKP" + assert sk.alg == "EdDSA" + assert sk.use == "sig" + assert sk.crv == "Ed25519" + assert "x" in sk.extras + as_dict = sk.to_dict() + assert as_dict["kid"] == "merchant-2026-05" + assert as_dict["x"] == gen.public_jwk["x"] + + def test_round_trip_es256(self) -> None: + gen = generate_ucp_signing_key(kid="es", alg="ES256") + sk = UCPSigningKey.from_jwk(gen.public_jwk) + assert sk.kty == "EC" + assert sk.crv == "P-256" + assert "x" in sk.extras and "y" in sk.extras + + def test_rejects_oct_symmetric_key(self) -> None: + with pytest.raises(ValueError, match=r"oct.*rejected|not a supported asymmetric key type"): + UCPSigningKey.from_jwk({"kid": "k", "kty": "oct", "k": "AAAA"}) + + def test_rejects_jwk_missing_kid(self) -> None: + with pytest.raises(ValueError, match="missing required field `kid`"): + UCPSigningKey.from_jwk({"kty": "OKP"}) + + def test_rejects_jwk_missing_kty(self) -> None: + with pytest.raises(ValueError, match="missing required field `kty`"): + UCPSigningKey.from_jwk({"kid": "k"}) + + def test_rejects_non_dict_input(self) -> None: + with pytest.raises(ValueError, match="expected a dict"): + UCPSigningKey.from_jwk("not a jwk") # type: ignore[arg-type] + + +class TestAdditionalHardening: + def test_sign_ucp_profile_rejects_kid_not_in_signing_keys(self) -> None: + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + with pytest.raises(ValueError, match=r"not present in profile.signing_keys"): + sign_ucp_profile(profile, signing_key=key.private_key, kid="wrong") + + def test_verify_rejects_malformed_jwks_missing_keys(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, {}) + assert exc.value.code == "malformed_jwks" + + def test_verify_rejects_non_dict_jwks(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, [key.public_jwk]) # type: ignore[arg-type] + assert exc.value.code == "malformed_jwks" + + def test_verify_rejects_non_dict_profile(self) -> None: + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile("not a profile", {"keys": []}) # type: ignore[arg-type] + assert exc.value.code == "no_signature" + + def test_verify_rejects_unusable_key_use_enc(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + enc_jwk = {**key.public_jwk, "use": "enc"} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([enc_jwk])) + assert exc.value.code == "unusable_key" + + def test_verify_rejects_unusable_key_alg_mismatch(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + # JWKS advertises the same kid but with a wrong `alg` (RFC 7517 §4.4 violation): + # JWS header carries alg=EdDSA, JWK declares alg=ES256. + wrong_alg_jwk = {**key.public_jwk, "alg": "ES256"} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([wrong_alg_jwk])) + assert exc.value.code == "unusable_key" + assert "ES256" in str(exc.value) + assert "EdDSA" in str(exc.value) + + @pytest.mark.parametrize("bad_sig", [42, None, [], {}]) + def test_verify_rejects_non_string_signature(self, bad_sig: object) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + tampered = {**profile, "signature": bad_sig} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, build_jwks_response([key.public_jwk])) + assert exc.value.code == "no_signature" + + @pytest.mark.parametrize("bad_entry", [None, "string"]) + def test_verify_rejects_non_dict_jwks_entry(self, bad_entry: object) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, {"keys": [bad_entry]}) + assert exc.value.code == "kid_not_found" + + def test_verify_rejects_protected_header_decoding_to_json_array(self) -> None: + import base64 + + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + header_array_b64 = ( + base64.urlsafe_b64encode(__import__("json").dumps(["EdDSA", "kid"]).encode()).rstrip(b"=").decode() + ) + bogus_jws = f"{header_array_b64}.payload.sig" + signed = {**profile, "signature": bogus_jws} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "malformed_jws" + + def test_verify_wraps_unrecognized_critical_header(self) -> None: + import base64 + + from joserfc import jws + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + # Hand-craft a JWS with crit (use the raw underlying key to bypass joserfc's sign-time check). + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + header = {"alg": "EdDSA", "kid": "k", "typ": "agentscore-profile+jws", "crit": ["fakething"], "fakething": "x"} + header_b64 = ( + base64.urlsafe_b64encode(__import__("json").dumps(header, separators=(",", ":")).encode()) + .rstrip(b"=") + .decode() + ) + payload_b64 = base64.urlsafe_b64encode(canonical).rstrip(b"=").decode() + signing_input = f"{header_b64}.{payload_b64}".encode() + sig = key.private_key.private_key.sign(signing_input) + sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b"=").decode() + jws_compact = f"{header_b64}.{payload_b64}.{sig_b64}" + + signed = {**profile, "signature": jws_compact} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "unrecognized_critical_header" + # Silence unused-import warnings — registry is referenced for the joserfc namespace. + _ = jws, JWSRegistry + + def test_verify_crit_with_missing_kid_emits_unrecognized_critical_header(self) -> None: + """JWS with both crit violation AND missing kid emits unrecognized_critical_header, + matching node-commerce's typ -> alg -> kid -> crit precedence (regression guard for + the round-17 cross-SDK parity gap).""" + import base64 + + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + # Hand-craft a JWS with header carrying both crit AND a kid that the JWKS does NOT contain. + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + header = { + "alg": "EdDSA", + "kid": "nonexistent", + "typ": "agentscore-profile+jws", + "crit": ["fakething"], + "fakething": "x", + } + header_b64 = ( + base64.urlsafe_b64encode(__import__("json").dumps(header, separators=(",", ":")).encode()) + .rstrip(b"=") + .decode() + ) + payload_b64 = base64.urlsafe_b64encode(canonical).rstrip(b"=").decode() + signing_input = f"{header_b64}.{payload_b64}".encode() + sig = key.private_key.private_key.sign(signing_input) + sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b"=").decode() + jws_compact = f"{header_b64}.{payload_b64}.{sig_b64}" + + signed = {**profile, "signature": jws_compact} + # JWKS contains 'real' but the JWS advertises kid='nonexistent'. Without the + # crit-before-kid-lookup check the verifier would emit kid_not_found, diverging + # from node-commerce. + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "unrecognized_critical_header" + + def _hand_craft_jws_with_crit(self, key: GeneratedUCPKey, profile: dict, crit_value: object) -> str: + """Build a JWS whose protected header carries an arbitrary `crit` value + (including JSON null / non-list shapes) by signing the raw bytes directly. + joserfc's high-level sign API would reject these on the way in.""" + import base64 + + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + header = {"alg": "EdDSA", "kid": "real", "typ": "agentscore-profile+jws", "crit": crit_value} + header_b64 = ( + base64.urlsafe_b64encode(__import__("json").dumps(header, separators=(",", ":")).encode()) + .rstrip(b"=") + .decode() + ) + payload_b64 = base64.urlsafe_b64encode(canonical).rstrip(b"=").decode() + signing_input = f"{header_b64}.{payload_b64}".encode() + sig = key.private_key.private_key.sign(signing_input) + sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b"=").decode() + return f"{header_b64}.{payload_b64}.{sig_b64}" + + def test_verify_crit_null_emits_malformed_jws(self) -> None: + """JWS protected header with crit=null is malformed (RFC 7515 §4.1.11 + requires a non-empty array). Regression guard: the previous `is not None` + gate let JSON null fall through to joserfc's iterate-crit, which crashed + with a raw TypeError instead of the typed UCPVerificationError. Node + sibling already maps crit=null to malformed_jws.""" + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + jws_compact = self._hand_craft_jws_with_crit(key, profile, None) + signed = {**profile, "signature": jws_compact} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "malformed_jws" + + def test_verify_crit_empty_array_emits_malformed_jws(self) -> None: + """RFC 7515 §4.1.11 requires `crit` be a non-empty array.""" + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + jws_compact = self._hand_craft_jws_with_crit(key, profile, []) + signed = {**profile, "signature": jws_compact} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "malformed_jws" + + def test_verify_crit_string_emits_malformed_jws(self) -> None: + """`crit` must be an array per RFC 7515 §4.1.11; a string is malformed.""" + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + jws_compact = self._hand_craft_jws_with_crit(key, profile, "fakething") + signed = {**profile, "signature": jws_compact} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "malformed_jws" + + @pytest.mark.parametrize( + "bad_crit", + [ + [42], + [None], + [{}], + [42, "valid"], + ["valid", 42], + ], + ) + def test_verify_crit_with_non_string_element_emits_malformed_jws(self, bad_crit: object) -> None: + """RFC 7515 §4.1.11: crit array entries MUST be strings. Non-string elements + (including mixed arrays) are malformed. Cross-language parity with node-commerce, + which rejects [42] etc. with malformed_jws.""" + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + jws_compact = self._hand_craft_jws_with_crit(key, profile, bad_crit) + signed = {**profile, "signature": jws_compact} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "malformed_jws" + + +class TestVerifierCanonicalizationTypedErrors: + """Verifier-side canonicalize must NEVER leak raw ValueError; always UCPVerificationError(body_mismatch).""" + + def _make_signed(self) -> tuple[dict, dict]: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + return signed, build_jwks_response([key.public_jwk]) + + def test_received_profile_with_float_raises_typed_body_mismatch(self) -> None: + signed, jwks = self._make_signed() + tampered = {**signed, "extras": {"n": 1.5}} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, jwks) + assert exc.value.code == "body_mismatch" + + def test_received_profile_with_oversized_int_raises_typed_body_mismatch(self) -> None: + signed, jwks = self._make_signed() + tampered = {**signed, "extras": {"n": 2**60}} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, jwks) + assert exc.value.code == "body_mismatch" + + def test_received_profile_with_nan_raises_typed_body_mismatch(self) -> None: + signed, jwks = self._make_signed() + tampered = {**signed, "extras": {"n": float("nan")}} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, jwks) + assert exc.value.code == "body_mismatch" + + +class TestRejectUnsafeNumbersDictKeys: + def test_sign_rejects_oversized_int_dict_key(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {2**60: "a"}} + with pytest.raises(ValueError, match="MAX_SAFE_INTEGER"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_sign_rejects_float_dict_key(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {1.5: "a"}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_sign_accepts_string_dict_keys_that_look_like_numbers(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"1.5": "a", "1152921504606846976": "b"}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + def test_sign_accepts_bool_dict_keys(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {True: "x"}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + +# U+2028 / U+2029 named via escape so the RUF001 ambiguous-character lint +# doesn't fire on the test inputs (the codepoints are intentional, not typos). +_U2028 = "\u2028" +_U2029 = "\u2029" + + +class TestLineParagraphSeparatorRejection: + """U+2028 / U+2029 are escaped by pre-ES2019 V8 (``JSON.stringify`` emits + the escaped sequences) but emitted raw by ``json.dumps(ensure_ascii=False)``. + + Modern V8 emits them raw too, so the divergence is theoretical on today's + Node, but the rejection mirrors core/api/src/lib/canonicalize.ts so the + contract stays symmetric for any pre-ES2019 verifier path (older V8, + browser-side verifier code). + """ + + def test_rejects_u2028_at_top_level(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"note": f"before{_U2028}after"}} + with pytest.raises(ValueError, match="U\\+2028"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2029_at_top_level(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"note": f"before{_U2029}after"}} + with pytest.raises(ValueError, match="U\\+2029"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2028_nested_in_list(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"items": ["ok", f"bad{_U2028}tail"]}} + with pytest.raises(ValueError, match="U\\+2028"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2029_nested_in_list(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"items": ["ok", f"bad{_U2029}tail"]}} + with pytest.raises(ValueError, match="U\\+2029"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2028_nested_in_dict_value(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"deep": {"inner": f"before{_U2028}after"}}} + with pytest.raises(ValueError, match="U\\+2028"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2029_nested_in_dict_value(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"deep": {"inner": f"before{_U2029}after"}}} + with pytest.raises(ValueError, match="U\\+2029"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2028_in_dict_key(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {f"bad{_U2028}key": "value"}} + with pytest.raises(ValueError, match="U\\+2028"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_accepts_u2027_sanity_case(self) -> None: + # U+2027 (HYPHENATION POINT) is a different codepoint, not a target of + # the rejection. Confirms we're matching exactly U+2028 / U+2029. + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"note": "before\u2027after"}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + +class TestJWKUseAlgNullTreatedAsAbsent: + """RFC 7517 lists ``use`` and ``alg`` as optional. Explicit JSON null is + out-of-spec but harmless; treat null as absent (skip-on-null) so a JWK + carrying ``"use": null`` or ``"alg": null`` matches the Node sibling's + ``!= null`` semantics in ucp-jwks.ts and the two languages stay + symmetric. + """ + + def test_verify_succeeds_when_matched_jwk_has_null_use(self) -> None: + key = generate_ucp_signing_key(kid="null-use") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="null-use") + jwks_with_null_use = build_jwks_response([{**key.public_jwk, "use": None}]) + assert verify_ucp_profile(signed, jwks_with_null_use) is True + + def test_verify_succeeds_when_matched_jwk_has_null_alg(self) -> None: + key = generate_ucp_signing_key(kid="null-alg", alg="EdDSA") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="null-alg") + jwks_with_null_alg = build_jwks_response([{**key.public_jwk, "alg": None}]) + assert verify_ucp_profile(signed, jwks_with_null_alg) is True + + def test_verify_still_rejects_use_enc_with_unusable_key(self) -> None: + # Sanity: non-null wrong values continue to fail with unusable_key. + key = generate_ucp_signing_key(kid="enc-sanity") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="enc-sanity") + enc_jwk = {**key.public_jwk, "use": "enc"} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([enc_jwk])) + assert exc.value.code == "unusable_key" + + +class TestVerifierErrorPrecedence: + def test_null_profile_with_malformed_jwks_returns_no_signature(self) -> None: + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(None, "not a jwks") # type: ignore[arg-type] + assert exc.value.code == "no_signature" diff --git a/uv.lock b/uv.lock index b949243..5a54e95 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ resolution-markers = [ [[package]] name = "agentscore-commerce" -version = "1.3.6" +version = "1.4.0" source = { editable = "." } dependencies = [ { name = "agentscore-py" }, @@ -46,6 +46,9 @@ starlette = [ stripe = [ { name = "stripe" }, ] +ucp = [ + { name = "joserfc" }, +] x402 = [ { name = "x402", extra = ["evm", "fastapi"] }, ] @@ -59,6 +62,7 @@ dev = [ { name = "django", version = "6.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "fastapi" }, { name = "flask" }, + { name = "joserfc" }, { name = "lefthook" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -83,13 +87,14 @@ requires-dist = [ { name = "fastapi", marker = "extra == 'fastapi'", specifier = ">=0.100.0" }, { name = "flask", marker = "extra == 'flask'", specifier = ">=2.0.0" }, { name = "httpx", specifier = ">=0.25.0,<1.0.0" }, + { name = "joserfc", marker = "extra == 'ucp'", specifier = ">=1.0.0,<2" }, { name = "pympp", extras = ["server", "tempo", "stripe"], marker = "extra == 'mppx'", specifier = ">=0.6,<1" }, { name = "sanic", marker = "extra == 'sanic'", specifier = ">=23.0.0" }, { name = "starlette", marker = "extra == 'starlette'", specifier = ">=0.27.0" }, { name = "stripe", marker = "extra == 'stripe'", specifier = ">=11.0.0" }, { name = "x402", extras = ["evm", "fastapi"], marker = "extra == 'x402'", specifier = ">=2.9,<3" }, ] -provides-extras = ["starlette", "fastapi", "flask", "django", "aiohttp", "sanic", "stripe", "x402", "mppx", "coinbase"] +provides-extras = ["starlette", "fastapi", "flask", "django", "aiohttp", "sanic", "stripe", "x402", "mppx", "coinbase", "ucp"] [package.metadata.requires-dev] dev = [ @@ -99,6 +104,7 @@ dev = [ { name = "django", specifier = ">=4.0" }, { name = "fastapi", specifier = ">=0.100.0" }, { name = "flask", specifier = ">=2.0.0" }, + { name = "joserfc", specifier = ">=1.0.0,<2" }, { name = "lefthook", specifier = ">=2.1.6" }, { name = "pytest", specifier = ">=7.0" }, { name = "pytest-asyncio", specifier = ">=0.21" }, @@ -699,101 +705,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, - { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, - { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, - { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, - { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, - { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, - { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, - { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, - { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, - { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, - { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, - { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, - { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, - { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, - { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, - { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, - { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, - { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, - { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, - { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, - { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, - { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, - { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, - { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, - { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +version = "7.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, + { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, ] [package.optional-dependencies] @@ -1600,6 +1606,18 @@ 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" }, ] +[[package]] +name = "joserfc" +version = "1.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz", hash = "sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48", size = 231881, upload-time = "2026-05-06T04:58:13.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/3b/ad1cb22e75c963b1f07c8a2329bf47227ce7e4361df5eb2fb101b2ce33ef/joserfc-1.6.5-py3-none-any.whl", hash = "sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e", size = 70464, upload-time = "2026-05-06T04:58:11.668Z" }, +] + [[package]] name = "jsonalias" version = "0.1.1" @@ -1624,14 +1642,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.1.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/5c/f3aedc83549aae71cd52b9e9687fe896e3dc6e966ba20eba04718605d198/markdown_it_py-4.1.0.tar.gz", hash = "sha256:760e3f87b2787c044c5138a5ba107b7c2be26c03b13cc7f8fe42756b65b1df6c", size = 81613, upload-time = "2026-05-06T16:32:13.649Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl", hash = "sha256:d4939a62a2dd0cd9cb80a191a711ba1d39bac8ed5ef9e9966895b0171c01c46d", size = 90955, upload-time = "2026-05-06T16:32:12.184Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -1887,101 +1905,113 @@ wheels = [ [[package]] name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] @@ -2160,16 +2190,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.0" +version = "2.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] [[package]] @@ -2300,11 +2330,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.27" +version = "0.0.28" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" }, ] [[package]] @@ -2392,106 +2422,106 @@ wheels = [ [[package]] name = "regex" -version = "2026.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/7a/617356cbecdb452812a5d42f720d6d5096b360d4a4c1073af700ea140ad2/regex-2026.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4c36a85b00fadb85db9d9e90144af0a980e1a3d2ef9cd0f8a5bef88054657c6", size = 489415, upload-time = "2026-04-03T20:53:11.645Z" }, - { url = "https://files.pythonhosted.org/packages/20/e6/bf057227144d02e3ba758b66649e87531d744dda5f3254f48660f18ae9d8/regex-2026.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5453ecf9cd58b562967badd1edbf092b0588a3af9e32ee3d05c985077ce87", size = 291205, upload-time = "2026-04-03T20:53:13.289Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3b/637181b787dd1a820ba1c712cee2b4144cd84a32dc776ca067b12b2d70c8/regex-2026.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6aa809ed4dc3706cc38594d67e641601bd2f36d5555b2780ff074edfcb136cf8", size = 289225, upload-time = "2026-04-03T20:53:16.002Z" }, - { url = "https://files.pythonhosted.org/packages/05/21/bac05d806ed02cd4b39d9c8e5b5f9a2998c94c3a351b7792e80671fa5315/regex-2026.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33424f5188a7db12958246a54f59a435b6cb62c5cf9c8d71f7cc49475a5fdada", size = 792434, upload-time = "2026-04-03T20:53:17.414Z" }, - { url = "https://files.pythonhosted.org/packages/d9/17/c65d1d8ae90b772d5758eb4014e1e011bb2db353fc4455432e6cc9100df7/regex-2026.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d346fccdde28abba117cc9edc696b9518c3307fbfcb689e549d9b5979018c6d", size = 861730, upload-time = "2026-04-03T20:53:18.903Z" }, - { url = "https://files.pythonhosted.org/packages/ad/64/933321aa082a2c6ee2785f22776143ba89840189c20d3b6b1d12b6aae16b/regex-2026.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:415a994b536440f5011aa77e50a4274d15da3245e876e5c7f19da349caaedd87", size = 906495, upload-time = "2026-04-03T20:53:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/01/ea/4c8d306e9c36ac22417336b1e02e7b358152c34dc379673f2d331143725f/regex-2026.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21e5eb86179b4c67b5759d452ea7c48eb135cd93308e7a260aa489ed2eb423a4", size = 799810, upload-time = "2026-04-03T20:53:22.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/ce/7605048f00e1379eba89d610c7d644d8f695dc9b26d3b6ecfa3132b872ff/regex-2026.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:312ec9dd1ae7d96abd8c5a36a552b2139931914407d26fba723f9e53c8186f86", size = 774242, upload-time = "2026-04-03T20:53:25.015Z" }, - { url = "https://files.pythonhosted.org/packages/e9/77/283e0d5023fde22cd9e86190d6d9beb21590a452b195ffe00274de470691/regex-2026.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0d2b28aa1354c7cd7f71b7658c4326f7facac106edd7f40eda984424229fd59", size = 781257, upload-time = "2026-04-03T20:53:26.918Z" }, - { url = "https://files.pythonhosted.org/packages/8b/fb/7f3b772be101373c8626ed34c5d727dcbb8abd42a7b1219bc25fd9a3cc04/regex-2026.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:349d7310eddff40429a099c08d995c6d4a4bfaf3ff40bd3b5e5cb5a5a3c7d453", size = 854490, upload-time = "2026-04-03T20:53:29.065Z" }, - { url = "https://files.pythonhosted.org/packages/85/30/56547b80f34f4dd2986e1cdd63b1712932f63b6c4ce2f79c50a6cd79d1c2/regex-2026.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e7ab63e9fe45a9ec3417509e18116b367e89c9ceb6219222a3396fa30b147f80", size = 763544, upload-time = "2026-04-03T20:53:30.917Z" }, - { url = "https://files.pythonhosted.org/packages/ac/2f/ce060fdfea8eff34a8997603532e44cdb7d1f35e3bc253612a8707a90538/regex-2026.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fe896e07a5a2462308297e515c0054e9ec2dd18dfdc9427b19900b37dfe6f40b", size = 844442, upload-time = "2026-04-03T20:53:32.463Z" }, - { url = "https://files.pythonhosted.org/packages/e5/44/810cb113096a1dacbe82789fbfab2823f79d19b7f1271acecb7009ba9b88/regex-2026.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb59c65069498dbae3c0ef07bbe224e1eaa079825a437fb47a479f0af11f774f", size = 789162, upload-time = "2026-04-03T20:53:34.039Z" }, - { url = "https://files.pythonhosted.org/packages/20/96/9647dd7f2ecf6d9ce1fb04dfdb66910d094e10d8fe53e9c15096d8aa0bd2/regex-2026.4.4-cp311-cp311-win32.whl", hash = "sha256:2a5d273181b560ef8397c8825f2b9d57013de744da9e8257b8467e5da8599351", size = 266227, upload-time = "2026-04-03T20:53:35.601Z" }, - { url = "https://files.pythonhosted.org/packages/33/80/74e13262460530c3097ff343a17de9a34d040a5dc4de9cf3a8241faab51c/regex-2026.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:9542ccc1e689e752594309444081582f7be2fdb2df75acafea8a075108566735", size = 278399, upload-time = "2026-04-03T20:53:37.021Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/39f19f47f19dcefa3403f09d13562ca1c0fd07ab54db2bc03148f3f6b46a/regex-2026.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5f9fb784824a042be3455b53d0b112655686fdb7a91f88f095f3fee1e2a2a54", size = 270473, upload-time = "2026-04-03T20:53:38.633Z" }, - { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, - { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, - { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, - { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, - { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, - { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, - { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, - { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, - { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, - { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, - { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, - { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, - { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, - { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, - { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, - { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, - { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, - { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, - { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, - { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, - { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, - { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, - { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, - { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, - { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, - { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, - { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, - { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, - { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, - { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, - { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, - { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, - { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, - { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, - { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, - { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, - { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, - { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, - { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, - { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, - { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, - { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, - { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, - { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, - { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, - { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, - { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, - { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, - { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, - { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, - { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, +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/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" }, ] [[package]] @@ -2946,26 +2976,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.34" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/69/e24eefe2c35c0fdbdec9b60e162727af669bb76d64d993d982eb67b24c38/ty-0.0.34.tar.gz", hash = "sha256:a6efe66b0f13c03a65e6c72ec9abfe2792e2fd063c74fa67e2c4930e29d661be", size = 5585933, upload-time = "2026-05-01T23:06:46.388Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/7b/8b85003d6639ef17a97dcbb31f4511cfe78f1c81a964470db100c8c883e7/ty-0.0.34-py3-none-linux_armv6l.whl", hash = "sha256:9ecc3d14f07a95a6ceb88e07f8e62358dbd37325d3d5bd56da7217ff1fef7fb8", size = 11067094, upload-time = "2026-05-01T23:06:21.133Z" }, - { url = "https://files.pythonhosted.org/packages/d7/25/b0098f65b020b015c40567c763fc66fffbec88b2ba6f584bca1e92f05ebb/ty-0.0.34-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0dccffd8a9d02321cd2dee3249df205e26d62694e741f4eeca36b157fd8b419f", size = 10840909, upload-time = "2026-05-01T23:06:18.409Z" }, - { url = "https://files.pythonhosted.org/packages/e4/55/5e4adcf7d2a1006b844903b27cb81244a9b748d850433a46a6c21776c401/ty-0.0.34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0ea47a2998e167ab3b21d2f4b5309a9cf33c297809f6d7e3e753252223174d0", size = 10279378, upload-time = "2026-05-01T23:06:37.962Z" }, - { url = "https://files.pythonhosted.org/packages/4d/91/f537dca0db8fe2558e8ab04d8941d687b384fcc1df5eb9023b2db75ac26c/ty-0.0.34-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37da00b41a118a459ae56d8947e70651073fb33ebfbceb820e4a10b22d5023", size = 10817423, upload-time = "2026-05-01T23:06:26.247Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c4/55a3ad1da2815af1009bdc1b8c90dc11a364cd314e4b48c5128ba9d38859/ty-0.0.34-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81cbbb93c2342fe3de43e625d3a9eb149633e9f485e816ebf6395d08685355d8", size = 10851826, upload-time = "2026-05-01T23:06:24.198Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/9c7606af22d73fb43ea4369472d9c66ece11231be73b0efe8e3c61655559/ty-0.0.34-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c5b4dea1594a021289e172582df9cde7089dce14b276fc650e7b212b1772e12", size = 11356318, upload-time = "2026-05-01T23:06:51.139Z" }, - { url = "https://files.pythonhosted.org/packages/20/54/bb423f663721ab4138b216425c6b55eaefd3a068243b24d6d8fe988f4e13/ty-0.0.34-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:030fb00aa2d2a5b5ae9d9183d574e0c82dae80566700a7490c43669d8ece40cd", size = 11902968, upload-time = "2026-05-01T23:06:35.82Z" }, - { url = "https://files.pythonhosted.org/packages/b6/22/01122b21ab6b534a2f618c6bbe5f1f7f49fd56f4b2ec8887cd6d40d08fb3/ty-0.0.34-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ae9555e24e36c63a8218e037a5a63f15579eb6aa94f41017e57cd41d335cfb5", size = 11548860, upload-time = "2026-05-01T23:06:42.155Z" }, - { url = "https://files.pythonhosted.org/packages/d1/50/86008b1392ec64bed1957bbcc7aaa43b466b50dfc91bb131841c21d7c5c3/ty-0.0.34-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99eb23df9ed129fc26d1ab00d6f0b8dfe5253b09c2ac6abdb11523fa70d67f10", size = 11457097, upload-time = "2026-05-01T23:06:53.477Z" }, - { url = "https://files.pythonhosted.org/packages/92/3e/4558b2296963ba99c58d8409c57d7db4f3061b656c3613cb21c02c1ef4c2/ty-0.0.34-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85de45382016eceae69e104815eb2cfa200787df104002e262a86cbd43ed2c02", size = 10798192, upload-time = "2026-05-01T23:06:40.004Z" }, - { url = "https://files.pythonhosted.org/packages/76/bf/650d24402be2ef678528d60caac1d9477a40fc37e3792ecef07834fd7a4a/ty-0.0.34-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:14cb575fb8fa5131f5129d100cfe23c1575d23faf5dfc5158432749a3e38c9b5", size = 10890390, upload-time = "2026-05-01T23:06:33.076Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ef/ccd2ca13906079f7935fd7e067661b24233017f57d987d51d6a121d85bb5/ty-0.0.34-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c6fc0b69d8450e6910ba9db34572b959b81329a97ae273c391f70e9fb6c1aade", size = 11031564, upload-time = "2026-05-01T23:06:55.812Z" }, - { url = "https://files.pythonhosted.org/packages/ba/2d/d27b72005b6f43599e3bcabab0d7135ac0c230b7a307bb99f9eea02c1cda/ty-0.0.34-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:30dfcec2f0fde3993f4f912ed0e057dcbebc8615299f610a4c2ddb7b5a3e1e06", size = 11553430, upload-time = "2026-05-01T23:06:31.096Z" }, - { url = "https://files.pythonhosted.org/packages/a7/12/20812e1ad930b8d4af70eebf19ad23cff6e31efcfa613ef884531fcdbaa1/ty-0.0.34-py3-none-win32.whl", hash = "sha256:97b77ddf007271b812a313a8f0a14929bc5590958433e1fb83ef585676f53342", size = 10436048, upload-time = "2026-05-01T23:06:49.108Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/afa095c5987868fbda27c0f731146ac8e3d07b357adfa83daccaee5b1a16/ty-0.0.34-py3-none-win_amd64.whl", hash = "sha256:1f543968accb952705134028d1fda8656882787dbbc667ad4d6c3ba23791d604", size = 11462526, upload-time = "2026-05-01T23:06:28.514Z" }, - { url = "https://files.pythonhosted.org/packages/63/8f/bf041a06260d77662c0605e56dacfe90b786bf824cbe1aed238d15fe5e84/ty-0.0.34-py3-none-win_arm64.whl", hash = "sha256:ea09108cbcb16b6b06d7596312b433bf49681e78d30e4dc7fb3c1b248a95e09a", size = 10846945, upload-time = "2026-05-01T23:06:44.428Z" }, +version = "0.0.35" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/53/440e7b1212c4b0abbd4adb7aed93f4971aa1f8dca386ac5515930afa9172/ty-0.0.35.tar.gz", hash = "sha256:8375c240ab38138a19db07996c9808fb7a92047c1492e1ce587c2ef5112ad3a9", size = 5629237, upload-time = "2026-05-10T18:25:17.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/84/19662ee881675815b7fafff940a365be1985730465afd9b75cb2edd5f8b3/ty-0.0.35-py3-none-linux_armv6l.whl", hash = "sha256:85ae1e59b9fb0b40e9d84fe61b29653c5f2f5e78b487ece371a7a38c20c781cf", size = 11198741, upload-time = "2026-05-10T18:24:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/62/df/7e5b6f83d85b4d2e5b72b5dceb388f440acc10679417bd46f829b9200fab/ty-0.0.35-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:709dbb7af4fcadb1196863c00b8791bbbbcc9dacbe15a0ff17f0af82b35d415b", size = 10948304, upload-time = "2026-05-10T18:24:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/59/94/72d7263aca055cde427f0ebcf08d6a74e5a5fee1d1e7fdd553696089cecb/ty-0.0.35-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2cb0877419ab0c8708b6925cb0c2800b263842bd3c425113f200538772f3a0cc", size = 10407413, upload-time = "2026-05-10T18:24:37.422Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/fda6fae8a81ce0cb5f24cdfe63260e110c7af8844e31fa07d1e6e8ef0232/ty-0.0.35-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7afbcfc61904b7e82e7fe1a1db832a40d8f01e69dee1775f6594e552980536c", size = 10932614, upload-time = "2026-05-10T18:24:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/72/3d/b98d8d4aa1a5ed6daaf15864e838f605ca7b1e8b93b7e17b96ed4bc4dfed/ty-0.0.35-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b61498cc3e4178031c079951257fbdb209a891b4feb10ad6c40f615a51846f41", size = 10962982, upload-time = "2026-05-10T18:24:44.88Z" }, + { url = "https://files.pythonhosted.org/packages/18/c4/2881aad71bf6fb2f8df17fc8e4bc89e904e54490a3ee747b5ef73f98ac85/ty-0.0.35-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:573b1eacda349fc8dba0d767b41631c3a6f66412363127c5bf2b1b40a1d898d2", size = 11476274, upload-time = "2026-05-10T18:24:42.4Z" }, + { url = "https://files.pythonhosted.org/packages/34/0f/7717650adaeaddd23eea70470e2c26d3f0b9b18fdc7f26ec9552d6001f17/ty-0.0.35-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7209746158d6393c1040aa64b3ca29622e212ea7d8bae22ba50dbcbb4f96f0a", size = 12012027, upload-time = "2026-05-10T18:25:00.752Z" }, + { url = "https://files.pythonhosted.org/packages/22/c9/1a16cb4aab6f4707d8f550772e91abc26d1c8870f19b5e2453ad10bb8209/ty-0.0.35-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4466a1470aa4418d49a9aa45d9da7de42033addd0a2837c5b2b0eb71d3c2bcd3", size = 11648894, upload-time = "2026-05-10T18:25:12.44Z" }, + { url = "https://files.pythonhosted.org/packages/18/a1/a977c0e07e9f88db9c67f90c6342a4dc4422c8091fa07bf26521870687c5/ty-0.0.35-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb44bb742d52c309dcaa6598bcf4d82eb4bf1241b9e4940461e522e30093fe8b", size = 11560482, upload-time = "2026-05-10T18:25:05.172Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c1/a5fb11227d5cc4ac3f29a115d8c8bc817578e8ef6907d1e4c914ddbf45ee/ty-0.0.35-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:34b219250736c989b2670a03782c61315f523f3a2be37f1f90b1207e2212c188", size = 11718495, upload-time = "2026-05-10T18:24:54.12Z" }, + { url = "https://files.pythonhosted.org/packages/3c/cb/e92e4317388b6d1fd821a46941b448a8a1ff0bf13e22147c5167d8fa1b00/ty-0.0.35-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:88e2ac497decc0940ef1a07571dee8a746112a93a09cdc7f8bca0099752e2e05", size = 10900815, upload-time = "2026-05-10T18:25:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4f/03bd87388a92567f262f35ac64e10d2be047d258f2dfcf1405f500fa2b90/ty-0.0.35-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:02cae51b53e6ec17d5d827ff1a3a76fd119705b56a92156e04399eda6e911596", size = 10998051, upload-time = "2026-05-10T18:25:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/b4/60/6edbc375ee6073973200096168f644e1081e5e55a7d42596826465b275de/ty-0.0.35-py3-none-musllinux_1_2_i686.whl", hash = "sha256:11871d730c9400d899ac0b9f3d660ed2e7e433377c8725549f8250a36a7f2620", size = 11148910, upload-time = "2026-05-10T18:24:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b1/a845d2066ed521c477450f436d4bd353d107e7c02dd6536a485944aaf892/ty-0.0.35-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ad0a2f0530d0933dcc99ad36ac556c63e384ea72ab9a18d23ad2e2c9fd61c73", size = 11671005, upload-time = "2026-05-10T18:24:56.223Z" }, + { url = "https://files.pythonhosted.org/packages/73/81/1d5912a54fb66b2f95ac828ae61d422ef5afeae1263e4d231e40796c229f/ty-0.0.35-py3-none-win32.whl", hash = "sha256:0e25d63ec4ab116e7f6757e44d16ca9216bca679d19ecc36d119cf80faada61a", size = 10481096, upload-time = "2026-05-10T18:24:39.976Z" }, + { url = "https://files.pythonhosted.org/packages/3b/36/1c7f8632bfec1c321f01581d4c940a3617b24bd3e8b37c8a7363d33fbfc4/ty-0.0.35-py3-none-win_amd64.whl", hash = "sha256:6a0a6d259f6f2f8f2f954c6f013d4e0b5eba68af6b353bf19a47d59ec254a3d5", size = 11555691, upload-time = "2026-05-10T18:25:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fb/59325221bce52f6e833d6865ce8360ef7d5e1e21151b38df6dc77c4327a7/ty-0.0.35-py3-none-win_arm64.whl", hash = "sha256:619c52c0fb2aa21961a848a1995135ad3b6d0a9aa54da0194e60f679cc200e13", size = 10925457, upload-time = "2026-05-10T18:25:10.352Z" }, ] [[package]] @@ -2985,14 +3016,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.33.0.20260503" +version = "2.33.0.20260508" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/b8/57e94268c0d82ac3eaa2fc35aa8ca7bbc2542f726b67dcf90b0b00a3b14d/types_requests-2.33.0.20260503.tar.gz", hash = "sha256:9721b2d9dbee7131f2fb39f20f0ebb1999c18cef4b512c9a7932f3722de7c5f4", size = 23931, upload-time = "2026-05-03T05:20:08.882Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/6b/eb226bdd61a982c9a03e02c657fb4ab001733506e6423906ac142331f2e3/types_requests-2.33.0.20260508.tar.gz", hash = "sha256:81b2ae5f0d20967714a6aa5ef9284c05570d7cb06b7de8f2a77b918b63ddd411", size = 23991, upload-time = "2026-05-08T04:50:56.818Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/82/959113a6351f3ca046cd0a8cd2cee071d7ea47473560557a01eeae9a6fe2/types_requests-2.33.0.20260503-py3-none-any.whl", hash = "sha256:02aaa7e3577a13471715bb1bddb693cc985ea514f754b503bf033e6a09a3e528", size = 20736, upload-time = "2026-05-03T05:20:07.858Z" }, + { url = "https://files.pythonhosted.org/packages/cb/96/080db0afdf2c5cc5fe512b41354e8d114fe8f65e9510c56ff8dfd40216ce/types_requests-2.33.0.20260508-py3-none-any.whl", hash = "sha256:fa01459cca184229713df03709db46a905325906d27e042cd4fd7ea3d15d3400", size = 20722, upload-time = "2026-05-08T04:50:55.548Z" }, ] [[package]] @@ -3082,11 +3113,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { 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" }, ] [[package]]