diff --git a/.github/workflows/request-promotion.yml b/.github/workflows/request-promotion.yml new file mode 100644 index 0000000..e47db98 --- /dev/null +++ b/.github/workflows/request-promotion.yml @@ -0,0 +1,69 @@ +name: request-promotion + +on: + workflow_dispatch: + inputs: + sha: + description: Commit SHA to promote; defaults to the current ref + required: false + type: string + workflow_run: + workflows: [ci] + types: [completed] + +permissions: + contents: read + +concurrency: + group: request-promotion-${{ github.event.workflow_run.head_sha || inputs.sha || github.sha }} + cancel-in-progress: false + +jobs: + request: + if: > + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'main' && + github.event.workflow_run.event == 'push') + runs-on: [self-hosted, linux, x64, hyrule-public-pr] + timeout-minutes: 5 + steps: + - name: Generate promotion app token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.PROMOTION_APP_ID }} + private-key: ${{ secrets.PROMOTION_APP_PRIVATE_KEY }} + owner: AS215932 + repositories: network-operations + + - name: Request network-operations promotion PR + env: + GH_TOKEN: ${{ github.token }} + PROMOTION_TOKEN: ${{ steps.app-token.outputs.token }} + REPOSITORY: ${{ github.repository }} + INPUT_SHA: ${{ inputs.sha }} + WORKFLOW_RUN_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + set -euo pipefail + app="${REPOSITORY#AS215932/}" + SHA="${INPUT_SHA:-${WORKFLOW_RUN_SHA:-${GITHUB_SHA}}}" + SHA="${SHA,,}" + + if ! [[ "$SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::promotion SHA must be a 40-character hex commit SHA" + exit 1 + fi + + canonical="$(gh api "repos/${REPOSITORY}/commits/${SHA}" --jq .sha)" + if [ "$canonical" != "$SHA" ]; then + echo "::error::commit ${SHA} was not found in ${REPOSITORY}" + exit 1 + fi + + GH_TOKEN="$PROMOTION_TOKEN" gh api --method POST repos/AS215932/network-operations/dispatches \ + -f event_type=app-promote \ + -f "client_payload[repository]=$REPOSITORY" \ + -f "client_payload[sha]=$SHA" \ + -f "client_payload[title]=Promote ${app} ${SHA:0:7}" \ + -f "client_payload[impact]=Automated promotion request from ${REPOSITORY}@${SHA}." diff --git a/README.md b/README.md index a3c92ae..2839474 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # agent-core -Shared, dependency-light **typed contracts** for the AS215932 Agent Runtime Framework. +Shared, dependency-light **typed contracts** and optional coordination services for +the AS215932 Agent Runtime Framework. -This is the **§31 safe milestone** (Phase 1) of the framework consolidation described in -`../docs/migration/first-safe-milestone.md`. It introduces standard contracts **without -changing any existing loop's behavior**: +The base installation remains contract-only; service and HTTP dependencies are isolated +behind extras: - `agent_core/contracts/` — pydantic v2 models (JSON-serializable, schema-versioned). Importing them pulls in **only pydantic** (no langgraph / pydantic-ai / db). @@ -16,13 +16,28 @@ changing any existing loop's behavior**: by tests only**; not wired into any loop's runtime. - `agent_core/contracts/graphs/` — *descriptive* draft `GraphSpec`s of the loops' current LangGraph topology (no compiler yet). +- `agent_core/coordination/` — signed LHP-v2 HTTP client used identically by SOC, NOC, + Engineering, Knowledge, and the Agentic Observatory. +- `agent_core/coordinator/` — optional FastAPI/Postgres coordination service. It owns + handoff state, claims, immutable approvals, verification transitions, and sanitized + case projections; it does not own private loop state or replace the trace collector. ## Scope -In: contracts, adapters (test-only), draft GraphSpecs, deterministic arbitration helper, -tests, CI. -Out (later phases): runtime, GraphSpec compiler, model router, tool/MCP registries, -memory store, learning substrate, judges, policy gates, control-plane API/GUI. +In: contracts, adapters, draft GraphSpecs, deterministic arbitration, the optional +coordinator/client, tests, and CI. Out: a universal agent runtime, GraphSpec compiler, +model router, tool/MCP registry, or direct production executor. + +## Coordinator + +```bash +uv run --extra coordinator agent-core-coordinator +``` + +Production requires `HYRULE_COORDINATOR_DATABASE_URL` and a JSON map of per-loop key +IDs/secrets in `HYRULE_COORDINATOR_LOOP_KEYS_JSON`. The service binds to port `8771` by +default. Every protected request is signed with the generic `X-Agent-Loop-*` headers; +the shared `CoordinatorClient` handles canonical encoding and signing. ## Develop diff --git a/agent_core/__init__.py b/agent_core/__init__.py index 1e9cebb..a2b0045 100644 --- a/agent_core/__init__.py +++ b/agent_core/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "0.7.0" +__version__ = "0.9.0" diff --git a/agent_core/contracts/__init__.py b/agent_core/contracts/__init__.py index 959341a..2c5fcc8 100644 --- a/agent_core/contracts/__init__.py +++ b/agent_core/contracts/__init__.py @@ -19,6 +19,22 @@ PolicyGateResult, ) from agent_core.contracts.context import RunContext, RuntimeContext +from agent_core.contracts.coordination import ( + CORE_LOOPS, + LHP_VERSION, + ApprovalRecord, + CaseProjection, + HandoffEnvelope, + HandoffEvent, + HandoffEventType, + HandoffRecord, + HandoffResult, + HandoffStatus, + LoopHeartbeat, + LoopRegistration, + ProbePlan, + VerificationResult, +) from agent_core.contracts.decision import DecisionPacket from agent_core.contracts.errors import ErrorEnvelope from agent_core.contracts.evidence import EvidencePacket, SourceRef @@ -111,6 +127,21 @@ "GovernanceControls", "LoopDecisionEnvelope", "CrossLoopArbiterDecision", + # LHP-v2 coordination + "CORE_LOOPS", + "LHP_VERSION", + "LoopRegistration", + "LoopHeartbeat", + "CaseProjection", + "HandoffEnvelope", + "HandoffEvent", + "HandoffEventType", + "HandoffResult", + "VerificationResult", + "ApprovalRecord", + "HandoffRecord", + "HandoffStatus", + "ProbePlan", # observatory "ObservatoryLink", "ActorRef", diff --git a/agent_core/contracts/coordination.py b/agent_core/contracts/coordination.py new file mode 100644 index 0000000..8b2d899 --- /dev/null +++ b/agent_core/contracts/coordination.py @@ -0,0 +1,259 @@ +"""Loop Handoff Protocol v2 coordination contracts. + +The contracts in this module are transport-neutral. They define the bounded, +sanitized objects exchanged through the coordinator without turning +``agent-core`` into an agent runtime. +""" + +from __future__ import annotations + +import hashlib +import json +from datetime import datetime, timedelta +from typing import Any, Literal +from uuid import uuid4 + +from pydantic import Field, field_validator, model_validator + +from agent_core.contracts._base import RiskLevel, VersionedModel, utcnow +from agent_core.contracts.evidence import SourceRef +from agent_core.contracts.governance import ApprovalTier + +LHP_VERSION = "lhp.v2" +CORE_LOOPS = frozenset({"soc", "noc", "engineering", "knowledge"}) + +HandoffStatus = Literal[ + "proposed", + "awaiting_approval", + "queued", + "claimed", + "in_progress", + "result_submitted", + "verification_pending", + "completed", + "rejected", + "failed", + "cancelled", + "expired", +] +HandoffEventType = Literal[ + "created", + "approval_requested", + "approved", + "rejected", + "queued", + "claimed", + "heartbeat", + "progress", + "result_submitted", + "verification_pending", + "verified", + "failed", + "cancelled", + "expired", +] + + +def _coordination_id(prefix: str) -> str: + return f"{prefix}_{uuid4().hex}" + + +def _canonical_hash(value: dict[str, Any]) -> str: + payload = json.dumps(value, sort_keys=True, separators=(",", ":"), default=str) + return hashlib.sha256(payload.encode("utf-8")).hexdigest() + + +class LoopRegistration(VersionedModel): + """Centrally approved loop identity and capability manifest.""" + + loop_id: str + display_name: str + capabilities: list[str] = Field(default_factory=list, max_length=100) + environment: str = "production" + service_name: str = "" + host: str = "" + enabled: bool = True + metadata: dict[str, Any] = Field(default_factory=dict) + + @field_validator("loop_id") + @classmethod + def _loop_id(cls, value: str) -> str: + value = value.strip().lower() + if not value or len(value) > 64: + raise ValueError("loop_id must be a non-empty token of at most 64 characters") + if any(ch not in "abcdefghijklmnopqrstuvwxyz0123456789_-" for ch in value): + raise ValueError("loop_id contains unsupported characters") + return value + + +class LoopHeartbeat(VersionedModel): + loop_id: str + status: Literal["active", "idle", "degraded", "disabled"] = "active" + summary: str = Field(default="", max_length=500) + observed_at: datetime = Field(default_factory=utcnow) + metadata: dict[str, Any] = Field(default_factory=dict) + + +class CaseProjection(VersionedModel): + """Sanitized shared view; the owner loop retains full authoritative state.""" + + case_id: str + owner_loop: str + version: int = Field(default=1, ge=1) + status: str = Field(default="open", max_length=80) + severity: str = Field(default="UNKNOWN", max_length=32) + title: str = Field(default="", max_length=300) + summary: str = Field(default="", max_length=2000) + resource_id: str = Field(default="", max_length=300) + evidence_refs: list[SourceRef] = Field(default_factory=list, max_length=40) + opened_at: datetime = Field(default_factory=utcnow) + updated_at: datetime = Field(default_factory=utcnow) + resolved_at: datetime | None = None + metadata: dict[str, Any] = Field(default_factory=dict) + + +class HandoffEnvelope(VersionedModel): + """Immutable, scope-hashed request passed from one loop to another.""" + + protocol_version: Literal["lhp.v2"] = "lhp.v2" + handoff_id: str = Field(default_factory=lambda: _coordination_id("handoff")) + work_item_id: str = "" + case_id: str = "" + source_loop: str + target_loop: str + capability: str = Field(min_length=1, max_length=160) + intent: str = Field(default="", max_length=1000) + summary: str = Field(default="", max_length=2000) + risk_level: RiskLevel = "low" + approval_tier: ApprovalTier = "none" + payload: dict[str, Any] = Field(default_factory=dict) + payload_ref: str = Field(default="", max_length=500) + evidence_refs: list[SourceRef] = Field(default_factory=list, max_length=40) + context_refs: list[SourceRef] = Field(default_factory=list, max_length=40) + constraints: dict[str, Any] = Field(default_factory=dict) + correlation_id: str = Field(default="", max_length=180) + causation_id: str = Field(default="", max_length=180) + run_id: str = Field(default="", max_length=180) + trace_id: str = Field(default="", max_length=180) + idempotency_key: str = Field(min_length=1, max_length=180) + created_at: datetime = Field(default_factory=utcnow) + expires_at: datetime | None = None + scope_hash: str = "" + + @field_validator("source_loop", "target_loop") + @classmethod + def _loop(cls, value: str) -> str: + value = value.strip().lower() + if not value or len(value) > 64: + raise ValueError("loop identity must be a non-empty token") + return value + + @model_validator(mode="after") + def _scope(self) -> HandoffEnvelope: + if self.source_loop == self.target_loop and self.capability != "soc.active_probe.rt2": + raise ValueError("self-targeted handoffs are reserved for approved SOC probe work") + material = self.model_dump(mode="json", exclude={"scope_hash", "created_at"}) + expected = _canonical_hash(material) + if self.scope_hash and self.scope_hash != expected: + raise ValueError("scope_hash does not match the canonical handoff scope") + self.scope_hash = expected + return self + + +class HandoffEvent(VersionedModel): + event_id: str = Field(default_factory=lambda: _coordination_id("hevt")) + handoff_id: str + event_type: HandoffEventType + actor_id: str + actor_loop: str = "" + from_status: HandoffStatus | None = None + to_status: HandoffStatus + handoff_version: int = Field(ge=1) + summary: str = Field(default="", max_length=1000) + payload: dict[str, Any] = Field(default_factory=dict) + created_at: datetime = Field(default_factory=utcnow) + + +class HandoffResult(VersionedModel): + handoff_id: str + result_id: str = Field(default_factory=lambda: _coordination_id("result")) + outcome: Literal["succeeded", "partial", "failed", "rejected"] + summary: str = Field(default="", max_length=2000) + evidence_refs: list[SourceRef] = Field(default_factory=list, max_length=40) + artifact_refs: list[SourceRef] = Field(default_factory=list, max_length=40) + payload: dict[str, Any] = Field(default_factory=dict) + completed_at: datetime = Field(default_factory=utcnow) + + +class VerificationResult(VersionedModel): + handoff_id: str + verification_id: str = Field(default_factory=lambda: _coordination_id("verify")) + verdict: Literal["passed", "failed", "pending"] + summary: str = Field(default="", max_length=2000) + evidence_refs: list[SourceRef] = Field(default_factory=list, max_length=40) + consecutive_passes: int = Field(default=0, ge=0) + required_consecutive_passes: int = Field(default=1, ge=1) + verified_at: datetime = Field(default_factory=utcnow) + + +class ApprovalRecord(VersionedModel): + approval_id: str = Field(default_factory=lambda: _coordination_id("approval")) + handoff_id: str + scope_hash: str = Field(min_length=64, max_length=64) + decision: Literal["approved", "rejected"] + approver_id: str + approver_login: str = "" + approver_role: Literal["operator", "senior"] + rationale: str = Field(default="", max_length=2000) + decided_at: datetime = Field(default_factory=utcnow) + expires_at: datetime | None = None + + +class HandoffRecord(VersionedModel): + envelope: HandoffEnvelope + status: HandoffStatus + version: int = Field(default=1, ge=1) + claim_owner: str = "" + lease_expires_at: datetime | None = None + result: HandoffResult | None = None + verification: VerificationResult | None = None + approval: ApprovalRecord | None = None + updated_at: datetime = Field(default_factory=utcnow) + + +class ProbePlan(VersionedModel): + """Bounded RT-2 plan. The coordinator approval binds its serialized hash.""" + + probe_kind: Literal[ + "tcp_connect_sweep", + "tls_handshake", + "http_headers", + "dns_consistency", + ] + targets: list[str] = Field(min_length=1, max_length=3) + ports: list[int] = Field(default_factory=list, max_length=32) + max_concurrency: int = Field(default=2, ge=1, le=2) + requests_per_second_per_target: float = Field(default=1.0, gt=0, le=1.0) + max_requests: int = Field(default=100, ge=1, le=100) + max_duration_seconds: int = Field(default=600, ge=1, le=600) + allow_redirects: bool = False + approved_asset_refs: list[SourceRef] = Field(min_length=1, max_length=20) + + @field_validator("ports") + @classmethod + def _ports(cls, value: list[int]) -> list[int]: + if any(port < 1 or port > 65535 for port in value): + raise ValueError("ports must be between 1 and 65535") + if len(set(value)) != len(value): + raise ValueError("ports must be unique") + return value + + @model_validator(mode="after") + def _probe_shape(self) -> ProbePlan: + if self.probe_kind in {"tcp_connect_sweep", "tls_handshake"} and not self.ports: + raise ValueError(f"{self.probe_kind} requires explicit ports") + return self + + +def default_lease_expiry(seconds: int = 120) -> datetime: + return utcnow() + timedelta(seconds=max(30, min(seconds, 900))) diff --git a/agent_core/contracts/schemas/ApprovalRecord.schema.json b/agent_core/contracts/schemas/ApprovalRecord.schema.json new file mode 100644 index 0000000..06c49ee --- /dev/null +++ b/agent_core/contracts/schemas/ApprovalRecord.schema.json @@ -0,0 +1,82 @@ +{ + "additionalProperties": false, + "properties": { + "approval_id": { + "title": "Approval Id", + "type": "string" + }, + "approver_id": { + "title": "Approver Id", + "type": "string" + }, + "approver_login": { + "default": "", + "title": "Approver Login", + "type": "string" + }, + "approver_role": { + "enum": [ + "operator", + "senior" + ], + "title": "Approver Role", + "type": "string" + }, + "decided_at": { + "format": "date-time", + "title": "Decided At", + "type": "string" + }, + "decision": { + "enum": [ + "approved", + "rejected" + ], + "title": "Decision", + "type": "string" + }, + "expires_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Expires At" + }, + "handoff_id": { + "title": "Handoff Id", + "type": "string" + }, + "rationale": { + "default": "", + "maxLength": 2000, + "title": "Rationale", + "type": "string" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "scope_hash": { + "maxLength": 64, + "minLength": 64, + "title": "Scope Hash", + "type": "string" + } + }, + "required": [ + "handoff_id", + "scope_hash", + "decision", + "approver_id", + "approver_role" + ], + "title": "ApprovalRecord", + "type": "object" +} diff --git a/agent_core/contracts/schemas/CaseProjection.schema.json b/agent_core/contracts/schemas/CaseProjection.schema.json new file mode 100644 index 0000000..be718fd --- /dev/null +++ b/agent_core/contracts/schemas/CaseProjection.schema.json @@ -0,0 +1,223 @@ +{ + "$defs": { + "SourceRef": { + "additionalProperties": false, + "description": "A cited source with optional A0-A5 authority (from the knowledge loop).", + "properties": { + "authority": { + "anyOf": [ + { + "enum": [ + "A0", + "A1", + "A2", + "A3", + "A4", + "A5" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Authority" + }, + "commit_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Commit Sha" + }, + "evidence_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Evidence Id" + }, + "excerpt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Excerpt" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Kind" + }, + "raw_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Ref" + }, + "ref": { + "title": "Ref", + "type": "string" + }, + "review_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Review Status" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "sensitivity_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sensitivity Class" + } + }, + "required": [ + "ref" + ], + "title": "SourceRef", + "type": "object" + } + }, + "additionalProperties": false, + "description": "Sanitized shared view; the owner loop retains full authoritative state.", + "properties": { + "case_id": { + "title": "Case Id", + "type": "string" + }, + "evidence_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 40, + "title": "Evidence Refs", + "type": "array" + }, + "metadata": { + "additionalProperties": true, + "title": "Metadata", + "type": "object" + }, + "opened_at": { + "format": "date-time", + "title": "Opened At", + "type": "string" + }, + "owner_loop": { + "title": "Owner Loop", + "type": "string" + }, + "resolved_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Resolved At" + }, + "resource_id": { + "default": "", + "maxLength": 300, + "title": "Resource Id", + "type": "string" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "severity": { + "default": "UNKNOWN", + "maxLength": 32, + "title": "Severity", + "type": "string" + }, + "status": { + "default": "open", + "maxLength": 80, + "title": "Status", + "type": "string" + }, + "summary": { + "default": "", + "maxLength": 2000, + "title": "Summary", + "type": "string" + }, + "title": { + "default": "", + "maxLength": 300, + "title": "Title", + "type": "string" + }, + "updated_at": { + "format": "date-time", + "title": "Updated At", + "type": "string" + }, + "version": { + "default": 1, + "minimum": 1, + "title": "Version", + "type": "integer" + } + }, + "required": [ + "case_id", + "owner_loop" + ], + "title": "CaseProjection", + "type": "object" +} diff --git a/agent_core/contracts/schemas/HandoffEnvelope.schema.json b/agent_core/contracts/schemas/HandoffEnvelope.schema.json new file mode 100644 index 0000000..ccd76f3 --- /dev/null +++ b/agent_core/contracts/schemas/HandoffEnvelope.schema.json @@ -0,0 +1,298 @@ +{ + "$defs": { + "SourceRef": { + "additionalProperties": false, + "description": "A cited source with optional A0-A5 authority (from the knowledge loop).", + "properties": { + "authority": { + "anyOf": [ + { + "enum": [ + "A0", + "A1", + "A2", + "A3", + "A4", + "A5" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Authority" + }, + "commit_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Commit Sha" + }, + "evidence_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Evidence Id" + }, + "excerpt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Excerpt" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Kind" + }, + "raw_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Ref" + }, + "ref": { + "title": "Ref", + "type": "string" + }, + "review_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Review Status" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "sensitivity_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sensitivity Class" + } + }, + "required": [ + "ref" + ], + "title": "SourceRef", + "type": "object" + } + }, + "additionalProperties": false, + "description": "Immutable, scope-hashed request passed from one loop to another.", + "properties": { + "approval_tier": { + "default": "none", + "enum": [ + "none", + "operator", + "senior", + "break_glass" + ], + "title": "Approval Tier", + "type": "string" + }, + "capability": { + "maxLength": 160, + "minLength": 1, + "title": "Capability", + "type": "string" + }, + "case_id": { + "default": "", + "title": "Case Id", + "type": "string" + }, + "causation_id": { + "default": "", + "maxLength": 180, + "title": "Causation Id", + "type": "string" + }, + "constraints": { + "additionalProperties": true, + "title": "Constraints", + "type": "object" + }, + "context_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 40, + "title": "Context Refs", + "type": "array" + }, + "correlation_id": { + "default": "", + "maxLength": 180, + "title": "Correlation Id", + "type": "string" + }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "evidence_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 40, + "title": "Evidence Refs", + "type": "array" + }, + "expires_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Expires At" + }, + "handoff_id": { + "title": "Handoff Id", + "type": "string" + }, + "idempotency_key": { + "maxLength": 180, + "minLength": 1, + "title": "Idempotency Key", + "type": "string" + }, + "intent": { + "default": "", + "maxLength": 1000, + "title": "Intent", + "type": "string" + }, + "payload": { + "additionalProperties": true, + "title": "Payload", + "type": "object" + }, + "payload_ref": { + "default": "", + "maxLength": 500, + "title": "Payload Ref", + "type": "string" + }, + "protocol_version": { + "const": "lhp.v2", + "default": "lhp.v2", + "title": "Protocol Version", + "type": "string" + }, + "risk_level": { + "default": "low", + "enum": [ + "low", + "medium", + "high", + "critical" + ], + "title": "Risk Level", + "type": "string" + }, + "run_id": { + "default": "", + "maxLength": 180, + "title": "Run Id", + "type": "string" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "scope_hash": { + "default": "", + "title": "Scope Hash", + "type": "string" + }, + "source_loop": { + "title": "Source Loop", + "type": "string" + }, + "summary": { + "default": "", + "maxLength": 2000, + "title": "Summary", + "type": "string" + }, + "target_loop": { + "title": "Target Loop", + "type": "string" + }, + "trace_id": { + "default": "", + "maxLength": 180, + "title": "Trace Id", + "type": "string" + }, + "work_item_id": { + "default": "", + "title": "Work Item Id", + "type": "string" + } + }, + "required": [ + "source_loop", + "target_loop", + "capability", + "idempotency_key" + ], + "title": "HandoffEnvelope", + "type": "object" +} diff --git a/agent_core/contracts/schemas/HandoffEvent.schema.json b/agent_core/contracts/schemas/HandoffEvent.schema.json new file mode 100644 index 0000000..9f8290c --- /dev/null +++ b/agent_core/contracts/schemas/HandoffEvent.schema.json @@ -0,0 +1,121 @@ +{ + "additionalProperties": false, + "properties": { + "actor_id": { + "title": "Actor Id", + "type": "string" + }, + "actor_loop": { + "default": "", + "title": "Actor Loop", + "type": "string" + }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "event_id": { + "title": "Event Id", + "type": "string" + }, + "event_type": { + "enum": [ + "created", + "approval_requested", + "approved", + "rejected", + "queued", + "claimed", + "heartbeat", + "progress", + "result_submitted", + "verification_pending", + "verified", + "failed", + "cancelled", + "expired" + ], + "title": "Event Type", + "type": "string" + }, + "from_status": { + "anyOf": [ + { + "enum": [ + "proposed", + "awaiting_approval", + "queued", + "claimed", + "in_progress", + "result_submitted", + "verification_pending", + "completed", + "rejected", + "failed", + "cancelled", + "expired" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "From Status" + }, + "handoff_id": { + "title": "Handoff Id", + "type": "string" + }, + "handoff_version": { + "minimum": 1, + "title": "Handoff Version", + "type": "integer" + }, + "payload": { + "additionalProperties": true, + "title": "Payload", + "type": "object" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "summary": { + "default": "", + "maxLength": 1000, + "title": "Summary", + "type": "string" + }, + "to_status": { + "enum": [ + "proposed", + "awaiting_approval", + "queued", + "claimed", + "in_progress", + "result_submitted", + "verification_pending", + "completed", + "rejected", + "failed", + "cancelled", + "expired" + ], + "title": "To Status", + "type": "string" + } + }, + "required": [ + "handoff_id", + "event_type", + "actor_id", + "to_status", + "handoff_version" + ], + "title": "HandoffEvent", + "type": "object" +} diff --git a/agent_core/contracts/schemas/HandoffRecord.schema.json b/agent_core/contracts/schemas/HandoffRecord.schema.json new file mode 100644 index 0000000..0024d99 --- /dev/null +++ b/agent_core/contracts/schemas/HandoffRecord.schema.json @@ -0,0 +1,609 @@ +{ + "$defs": { + "ApprovalRecord": { + "additionalProperties": false, + "properties": { + "approval_id": { + "title": "Approval Id", + "type": "string" + }, + "approver_id": { + "title": "Approver Id", + "type": "string" + }, + "approver_login": { + "default": "", + "title": "Approver Login", + "type": "string" + }, + "approver_role": { + "enum": [ + "operator", + "senior" + ], + "title": "Approver Role", + "type": "string" + }, + "decided_at": { + "format": "date-time", + "title": "Decided At", + "type": "string" + }, + "decision": { + "enum": [ + "approved", + "rejected" + ], + "title": "Decision", + "type": "string" + }, + "expires_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Expires At" + }, + "handoff_id": { + "title": "Handoff Id", + "type": "string" + }, + "rationale": { + "default": "", + "maxLength": 2000, + "title": "Rationale", + "type": "string" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "scope_hash": { + "maxLength": 64, + "minLength": 64, + "title": "Scope Hash", + "type": "string" + } + }, + "required": [ + "handoff_id", + "scope_hash", + "decision", + "approver_id", + "approver_role" + ], + "title": "ApprovalRecord", + "type": "object" + }, + "HandoffEnvelope": { + "additionalProperties": false, + "description": "Immutable, scope-hashed request passed from one loop to another.", + "properties": { + "approval_tier": { + "default": "none", + "enum": [ + "none", + "operator", + "senior", + "break_glass" + ], + "title": "Approval Tier", + "type": "string" + }, + "capability": { + "maxLength": 160, + "minLength": 1, + "title": "Capability", + "type": "string" + }, + "case_id": { + "default": "", + "title": "Case Id", + "type": "string" + }, + "causation_id": { + "default": "", + "maxLength": 180, + "title": "Causation Id", + "type": "string" + }, + "constraints": { + "additionalProperties": true, + "title": "Constraints", + "type": "object" + }, + "context_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 40, + "title": "Context Refs", + "type": "array" + }, + "correlation_id": { + "default": "", + "maxLength": 180, + "title": "Correlation Id", + "type": "string" + }, + "created_at": { + "format": "date-time", + "title": "Created At", + "type": "string" + }, + "evidence_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 40, + "title": "Evidence Refs", + "type": "array" + }, + "expires_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Expires At" + }, + "handoff_id": { + "title": "Handoff Id", + "type": "string" + }, + "idempotency_key": { + "maxLength": 180, + "minLength": 1, + "title": "Idempotency Key", + "type": "string" + }, + "intent": { + "default": "", + "maxLength": 1000, + "title": "Intent", + "type": "string" + }, + "payload": { + "additionalProperties": true, + "title": "Payload", + "type": "object" + }, + "payload_ref": { + "default": "", + "maxLength": 500, + "title": "Payload Ref", + "type": "string" + }, + "protocol_version": { + "const": "lhp.v2", + "default": "lhp.v2", + "title": "Protocol Version", + "type": "string" + }, + "risk_level": { + "default": "low", + "enum": [ + "low", + "medium", + "high", + "critical" + ], + "title": "Risk Level", + "type": "string" + }, + "run_id": { + "default": "", + "maxLength": 180, + "title": "Run Id", + "type": "string" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "scope_hash": { + "default": "", + "title": "Scope Hash", + "type": "string" + }, + "source_loop": { + "title": "Source Loop", + "type": "string" + }, + "summary": { + "default": "", + "maxLength": 2000, + "title": "Summary", + "type": "string" + }, + "target_loop": { + "title": "Target Loop", + "type": "string" + }, + "trace_id": { + "default": "", + "maxLength": 180, + "title": "Trace Id", + "type": "string" + }, + "work_item_id": { + "default": "", + "title": "Work Item Id", + "type": "string" + } + }, + "required": [ + "source_loop", + "target_loop", + "capability", + "idempotency_key" + ], + "title": "HandoffEnvelope", + "type": "object" + }, + "HandoffResult": { + "additionalProperties": false, + "properties": { + "artifact_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 40, + "title": "Artifact Refs", + "type": "array" + }, + "completed_at": { + "format": "date-time", + "title": "Completed At", + "type": "string" + }, + "evidence_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 40, + "title": "Evidence Refs", + "type": "array" + }, + "handoff_id": { + "title": "Handoff Id", + "type": "string" + }, + "outcome": { + "enum": [ + "succeeded", + "partial", + "failed", + "rejected" + ], + "title": "Outcome", + "type": "string" + }, + "payload": { + "additionalProperties": true, + "title": "Payload", + "type": "object" + }, + "result_id": { + "title": "Result Id", + "type": "string" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "summary": { + "default": "", + "maxLength": 2000, + "title": "Summary", + "type": "string" + } + }, + "required": [ + "handoff_id", + "outcome" + ], + "title": "HandoffResult", + "type": "object" + }, + "SourceRef": { + "additionalProperties": false, + "description": "A cited source with optional A0-A5 authority (from the knowledge loop).", + "properties": { + "authority": { + "anyOf": [ + { + "enum": [ + "A0", + "A1", + "A2", + "A3", + "A4", + "A5" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Authority" + }, + "commit_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Commit Sha" + }, + "evidence_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Evidence Id" + }, + "excerpt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Excerpt" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Kind" + }, + "raw_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Ref" + }, + "ref": { + "title": "Ref", + "type": "string" + }, + "review_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Review Status" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "sensitivity_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sensitivity Class" + } + }, + "required": [ + "ref" + ], + "title": "SourceRef", + "type": "object" + }, + "VerificationResult": { + "additionalProperties": false, + "properties": { + "consecutive_passes": { + "default": 0, + "minimum": 0, + "title": "Consecutive Passes", + "type": "integer" + }, + "evidence_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 40, + "title": "Evidence Refs", + "type": "array" + }, + "handoff_id": { + "title": "Handoff Id", + "type": "string" + }, + "required_consecutive_passes": { + "default": 1, + "minimum": 1, + "title": "Required Consecutive Passes", + "type": "integer" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "summary": { + "default": "", + "maxLength": 2000, + "title": "Summary", + "type": "string" + }, + "verdict": { + "enum": [ + "passed", + "failed", + "pending" + ], + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "title": "Verification Id", + "type": "string" + }, + "verified_at": { + "format": "date-time", + "title": "Verified At", + "type": "string" + } + }, + "required": [ + "handoff_id", + "verdict" + ], + "title": "VerificationResult", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "approval": { + "anyOf": [ + { + "$ref": "#/$defs/ApprovalRecord" + }, + { + "type": "null" + } + ], + "default": null + }, + "claim_owner": { + "default": "", + "title": "Claim Owner", + "type": "string" + }, + "envelope": { + "$ref": "#/$defs/HandoffEnvelope" + }, + "lease_expires_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lease Expires At" + }, + "result": { + "anyOf": [ + { + "$ref": "#/$defs/HandoffResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "status": { + "enum": [ + "proposed", + "awaiting_approval", + "queued", + "claimed", + "in_progress", + "result_submitted", + "verification_pending", + "completed", + "rejected", + "failed", + "cancelled", + "expired" + ], + "title": "Status", + "type": "string" + }, + "updated_at": { + "format": "date-time", + "title": "Updated At", + "type": "string" + }, + "verification": { + "anyOf": [ + { + "$ref": "#/$defs/VerificationResult" + }, + { + "type": "null" + } + ], + "default": null + }, + "version": { + "default": 1, + "minimum": 1, + "title": "Version", + "type": "integer" + } + }, + "required": [ + "envelope", + "status" + ], + "title": "HandoffRecord", + "type": "object" +} diff --git a/agent_core/contracts/schemas/HandoffResult.schema.json b/agent_core/contracts/schemas/HandoffResult.schema.json new file mode 100644 index 0000000..a29d2ca --- /dev/null +++ b/agent_core/contracts/schemas/HandoffResult.schema.json @@ -0,0 +1,192 @@ +{ + "$defs": { + "SourceRef": { + "additionalProperties": false, + "description": "A cited source with optional A0-A5 authority (from the knowledge loop).", + "properties": { + "authority": { + "anyOf": [ + { + "enum": [ + "A0", + "A1", + "A2", + "A3", + "A4", + "A5" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Authority" + }, + "commit_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Commit Sha" + }, + "evidence_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Evidence Id" + }, + "excerpt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Excerpt" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Kind" + }, + "raw_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Ref" + }, + "ref": { + "title": "Ref", + "type": "string" + }, + "review_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Review Status" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "sensitivity_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sensitivity Class" + } + }, + "required": [ + "ref" + ], + "title": "SourceRef", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "artifact_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 40, + "title": "Artifact Refs", + "type": "array" + }, + "completed_at": { + "format": "date-time", + "title": "Completed At", + "type": "string" + }, + "evidence_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 40, + "title": "Evidence Refs", + "type": "array" + }, + "handoff_id": { + "title": "Handoff Id", + "type": "string" + }, + "outcome": { + "enum": [ + "succeeded", + "partial", + "failed", + "rejected" + ], + "title": "Outcome", + "type": "string" + }, + "payload": { + "additionalProperties": true, + "title": "Payload", + "type": "object" + }, + "result_id": { + "title": "Result Id", + "type": "string" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "summary": { + "default": "", + "maxLength": 2000, + "title": "Summary", + "type": "string" + } + }, + "required": [ + "handoff_id", + "outcome" + ], + "title": "HandoffResult", + "type": "object" +} diff --git a/agent_core/contracts/schemas/LoopHeartbeat.schema.json b/agent_core/contracts/schemas/LoopHeartbeat.schema.json new file mode 100644 index 0000000..74145c9 --- /dev/null +++ b/agent_core/contracts/schemas/LoopHeartbeat.schema.json @@ -0,0 +1,46 @@ +{ + "additionalProperties": false, + "properties": { + "loop_id": { + "title": "Loop Id", + "type": "string" + }, + "metadata": { + "additionalProperties": true, + "title": "Metadata", + "type": "object" + }, + "observed_at": { + "format": "date-time", + "title": "Observed At", + "type": "string" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "status": { + "default": "active", + "enum": [ + "active", + "idle", + "degraded", + "disabled" + ], + "title": "Status", + "type": "string" + }, + "summary": { + "default": "", + "maxLength": 500, + "title": "Summary", + "type": "string" + } + }, + "required": [ + "loop_id" + ], + "title": "LoopHeartbeat", + "type": "object" +} diff --git a/agent_core/contracts/schemas/LoopRegistration.schema.json b/agent_core/contracts/schemas/LoopRegistration.schema.json new file mode 100644 index 0000000..4e3c551 --- /dev/null +++ b/agent_core/contracts/schemas/LoopRegistration.schema.json @@ -0,0 +1,58 @@ +{ + "additionalProperties": false, + "description": "Centrally approved loop identity and capability manifest.", + "properties": { + "capabilities": { + "items": { + "type": "string" + }, + "maxItems": 100, + "title": "Capabilities", + "type": "array" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "enabled": { + "default": true, + "title": "Enabled", + "type": "boolean" + }, + "environment": { + "default": "production", + "title": "Environment", + "type": "string" + }, + "host": { + "default": "", + "title": "Host", + "type": "string" + }, + "loop_id": { + "title": "Loop Id", + "type": "string" + }, + "metadata": { + "additionalProperties": true, + "title": "Metadata", + "type": "object" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "service_name": { + "default": "", + "title": "Service Name", + "type": "string" + } + }, + "required": [ + "loop_id", + "display_name" + ], + "title": "LoopRegistration", + "type": "object" +} diff --git a/agent_core/contracts/schemas/ProbePlan.schema.json b/agent_core/contracts/schemas/ProbePlan.schema.json new file mode 100644 index 0000000..d7600d6 --- /dev/null +++ b/agent_core/contracts/schemas/ProbePlan.schema.json @@ -0,0 +1,213 @@ +{ + "$defs": { + "SourceRef": { + "additionalProperties": false, + "description": "A cited source with optional A0-A5 authority (from the knowledge loop).", + "properties": { + "authority": { + "anyOf": [ + { + "enum": [ + "A0", + "A1", + "A2", + "A3", + "A4", + "A5" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Authority" + }, + "commit_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Commit Sha" + }, + "evidence_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Evidence Id" + }, + "excerpt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Excerpt" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Kind" + }, + "raw_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Ref" + }, + "ref": { + "title": "Ref", + "type": "string" + }, + "review_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Review Status" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "sensitivity_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sensitivity Class" + } + }, + "required": [ + "ref" + ], + "title": "SourceRef", + "type": "object" + } + }, + "additionalProperties": false, + "description": "Bounded RT-2 plan. The coordinator approval binds its serialized hash.", + "properties": { + "allow_redirects": { + "default": false, + "title": "Allow Redirects", + "type": "boolean" + }, + "approved_asset_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 20, + "minItems": 1, + "title": "Approved Asset Refs", + "type": "array" + }, + "max_concurrency": { + "default": 2, + "maximum": 2, + "minimum": 1, + "title": "Max Concurrency", + "type": "integer" + }, + "max_duration_seconds": { + "default": 600, + "maximum": 600, + "minimum": 1, + "title": "Max Duration Seconds", + "type": "integer" + }, + "max_requests": { + "default": 100, + "maximum": 100, + "minimum": 1, + "title": "Max Requests", + "type": "integer" + }, + "ports": { + "items": { + "type": "integer" + }, + "maxItems": 32, + "title": "Ports", + "type": "array" + }, + "probe_kind": { + "enum": [ + "tcp_connect_sweep", + "tls_handshake", + "http_headers", + "dns_consistency" + ], + "title": "Probe Kind", + "type": "string" + }, + "requests_per_second_per_target": { + "default": 1.0, + "exclusiveMinimum": 0, + "maximum": 1.0, + "title": "Requests Per Second Per Target", + "type": "number" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "targets": { + "items": { + "type": "string" + }, + "maxItems": 3, + "minItems": 1, + "title": "Targets", + "type": "array" + } + }, + "required": [ + "probe_kind", + "targets", + "approved_asset_refs" + ], + "title": "ProbePlan", + "type": "object" +} diff --git a/agent_core/contracts/schemas/VerificationResult.schema.json b/agent_core/contracts/schemas/VerificationResult.schema.json new file mode 100644 index 0000000..81684f0 --- /dev/null +++ b/agent_core/contracts/schemas/VerificationResult.schema.json @@ -0,0 +1,190 @@ +{ + "$defs": { + "SourceRef": { + "additionalProperties": false, + "description": "A cited source with optional A0-A5 authority (from the knowledge loop).", + "properties": { + "authority": { + "anyOf": [ + { + "enum": [ + "A0", + "A1", + "A2", + "A3", + "A4", + "A5" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Authority" + }, + "commit_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Commit Sha" + }, + "evidence_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Evidence Id" + }, + "excerpt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Excerpt" + }, + "kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Kind" + }, + "raw_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Ref" + }, + "ref": { + "title": "Ref", + "type": "string" + }, + "review_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Review Status" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "sensitivity_class": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sensitivity Class" + } + }, + "required": [ + "ref" + ], + "title": "SourceRef", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "consecutive_passes": { + "default": 0, + "minimum": 0, + "title": "Consecutive Passes", + "type": "integer" + }, + "evidence_refs": { + "items": { + "$ref": "#/$defs/SourceRef" + }, + "maxItems": 40, + "title": "Evidence Refs", + "type": "array" + }, + "handoff_id": { + "title": "Handoff Id", + "type": "string" + }, + "required_consecutive_passes": { + "default": 1, + "minimum": 1, + "title": "Required Consecutive Passes", + "type": "integer" + }, + "schema_version": { + "default": "0.1.0", + "title": "Schema Version", + "type": "string" + }, + "summary": { + "default": "", + "maxLength": 2000, + "title": "Summary", + "type": "string" + }, + "verdict": { + "enum": [ + "passed", + "failed", + "pending" + ], + "title": "Verdict", + "type": "string" + }, + "verification_id": { + "title": "Verification Id", + "type": "string" + }, + "verified_at": { + "format": "date-time", + "title": "Verified At", + "type": "string" + } + }, + "required": [ + "handoff_id", + "verdict" + ], + "title": "VerificationResult", + "type": "object" +} diff --git a/agent_core/coordination/__init__.py b/agent_core/coordination/__init__.py new file mode 100644 index 0000000..1a61c4f --- /dev/null +++ b/agent_core/coordination/__init__.py @@ -0,0 +1,10 @@ +"""LHP-v2 client helpers. + +Importing this package requires the optional ``coordination-client`` extra; +the core contracts remain dependency-light. +""" + +from agent_core.coordination.auth import LoopRequestSigner, build_signature +from agent_core.coordination.client import CoordinatorClient, CoordinatorError + +__all__ = ["CoordinatorClient", "CoordinatorError", "LoopRequestSigner", "build_signature"] diff --git a/agent_core/coordination/auth.py b/agent_core/coordination/auth.py new file mode 100644 index 0000000..8bf0ef2 --- /dev/null +++ b/agent_core/coordination/auth.py @@ -0,0 +1,71 @@ +"""Per-loop request signing for coordinator HTTP traffic.""" + +from __future__ import annotations + +import hashlib +import hmac +import secrets +from dataclasses import dataclass +from datetime import UTC, datetime + + +def body_digest(body: bytes) -> str: + return hashlib.sha256(body).hexdigest() + + +def signature_message( + *, method: str, path: str, timestamp: str, nonce: str, key_id: str, body: bytes +) -> bytes: + return "\n".join( + [method.upper(), path, timestamp, nonce, key_id, body_digest(body)] + ).encode("utf-8") + + +def build_signature( + *, + secret: str, + method: str, + path: str, + timestamp: str, + nonce: str, + key_id: str, + body: bytes, +) -> str: + return hmac.new( + secret.encode("utf-8"), + signature_message( + method=method, + path=path, + timestamp=timestamp, + nonce=nonce, + key_id=key_id, + body=body, + ), + hashlib.sha256, + ).hexdigest() + + +@dataclass(frozen=True) +class LoopRequestSigner: + loop_id: str + key_id: str + secret: str + + def headers(self, *, method: str, path: str, body: bytes = b"") -> dict[str, str]: + timestamp = datetime.now(UTC).isoformat() + nonce = secrets.token_urlsafe(24) + return { + "X-Agent-Loop-Identity": self.loop_id, + "X-Agent-Loop-Key-Id": self.key_id, + "X-Agent-Loop-Timestamp": timestamp, + "X-Agent-Loop-Nonce": nonce, + "X-Agent-Loop-Signature": build_signature( + secret=self.secret, + method=method, + path=path, + timestamp=timestamp, + nonce=nonce, + key_id=self.key_id, + body=body, + ), + } diff --git a/agent_core/coordination/client.py b/agent_core/coordination/client.py new file mode 100644 index 0000000..54d8008 --- /dev/null +++ b/agent_core/coordination/client.py @@ -0,0 +1,206 @@ +"""Async, signed client for the agent-core coordination service.""" + +from __future__ import annotations + +import json +import os +from typing import Any + +import httpx +from pydantic import BaseModel + +from agent_core.contracts.coordination import ( + ApprovalRecord, + CaseProjection, + HandoffEnvelope, + HandoffRecord, + HandoffResult, + LoopHeartbeat, + VerificationResult, +) +from agent_core.coordination.auth import LoopRequestSigner + + +class CoordinatorError(RuntimeError): + """Coordinator request failed or returned an invalid response.""" + + +def _json_bytes(value: BaseModel | dict[str, Any] | None) -> bytes: + if value is None: + return b"" + payload = value.model_dump(mode="json") if isinstance(value, BaseModel) else value + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() + + +class CoordinatorClient: + def __init__( + self, + base_url: str, + *, + signer: LoopRequestSigner, + timeout: float = 15.0, + transport: httpx.AsyncBaseTransport | None = None, + ) -> None: + self.base_url = base_url.rstrip("/") + self.signer = signer + self.timeout = timeout + self.transport = transport + + @classmethod + def from_env(cls, loop_id: str, *, prefix: str = "HYRULE_COORDINATOR") -> CoordinatorClient: + base_url = os.environ.get(f"{prefix}_URL", "").strip() + key_id = os.environ.get(f"{prefix}_KEY_ID", "default").strip() + secret = os.environ.get(f"{prefix}_SECRET", "").strip() + if not base_url or not secret: + raise CoordinatorError(f"{prefix}_URL and {prefix}_SECRET are required") + return cls( + base_url, + signer=LoopRequestSigner(loop_id=loop_id, key_id=key_id, secret=secret), + ) + + async def _request( + self, + method: str, + path: str, + *, + payload: BaseModel | dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + ) -> Any: + body = _json_bytes(payload) + headers = self.signer.headers(method=method, path=path, body=body) + if body: + headers["Content-Type"] = "application/json" + async with httpx.AsyncClient( + base_url=self.base_url, + timeout=self.timeout, + transport=self.transport, + ) as client: + response = await client.request( + method, + path, + content=body or None, + params=params, + headers=headers, + ) + if response.status_code >= 400: + try: + detail = response.json().get("detail", response.text) + except Exception: + detail = response.text + raise CoordinatorError( + f"coordinator {method} {path} returned {response.status_code}: {detail}" + ) + if not response.content: + return None + return response.json() + + async def health(self) -> dict[str, Any]: + return dict(await self._request("GET", "/healthz")) + + async def loops(self) -> list[dict[str, Any]]: + data = await self._request("GET", "/v1/loops") + return list(data.get("loops", [])) + + async def heartbeat(self, heartbeat: LoopHeartbeat) -> dict[str, Any]: + return dict( + await self._request( + "POST", f"/v1/loops/{heartbeat.loop_id}/heartbeat", payload=heartbeat + ) + ) + + async def put_case(self, projection: CaseProjection) -> CaseProjection: + data = await self._request( + "PUT", f"/v1/cases/{projection.case_id}", payload=projection + ) + return CaseProjection.model_validate(data) + + async def cases(self, **filters: Any) -> list[CaseProjection]: + data = await self._request("GET", "/v1/cases", params=filters) + return [CaseProjection.model_validate(item) for item in data.get("cases", [])] + + async def case(self, case_id: str) -> CaseProjection: + return CaseProjection.model_validate( + await self._request("GET", f"/v1/cases/{case_id}") + ) + + async def create_handoff(self, envelope: HandoffEnvelope) -> HandoffRecord: + return HandoffRecord.model_validate( + await self._request("POST", "/v1/handoffs", payload=envelope) + ) + + async def handoff(self, handoff_id: str) -> HandoffRecord: + return HandoffRecord.model_validate( + await self._request("GET", f"/v1/handoffs/{handoff_id}") + ) + + async def handoffs(self, **filters: Any) -> list[HandoffRecord]: + data = await self._request("GET", "/v1/handoffs", params=filters) + return [HandoffRecord.model_validate(item) for item in data.get("handoffs", [])] + + async def handoff_events(self, handoff_id: str) -> list[dict[str, Any]]: + data = await self._request("GET", f"/v1/handoffs/{handoff_id}/events") + return list(data.get("events", [])) + + async def inbox(self, *, status: str | None = None, limit: int = 100) -> list[HandoffRecord]: + params: dict[str, Any] = {"limit": limit} + if status: + params["status"] = status + data = await self._request("GET", "/v1/inbox", params=params) + return [HandoffRecord.model_validate(item) for item in data.get("handoffs", [])] + + async def claim(self, handoff_id: str, *, lease_seconds: int = 120) -> HandoffRecord: + return HandoffRecord.model_validate( + await self._request( + "POST", + f"/v1/handoffs/{handoff_id}/claim", + payload={"lease_seconds": lease_seconds}, + ) + ) + + async def heartbeat_claim(self, handoff_id: str, *, lease_seconds: int = 120) -> HandoffRecord: + return HandoffRecord.model_validate( + await self._request( + "POST", + f"/v1/handoffs/{handoff_id}/heartbeat", + payload={"lease_seconds": lease_seconds}, + ) + ) + + async def progress(self, handoff_id: str, summary: str) -> HandoffRecord: + return HandoffRecord.model_validate( + await self._request( + "POST", f"/v1/handoffs/{handoff_id}/progress", payload={"summary": summary} + ) + ) + + async def submit_result(self, result: HandoffResult) -> HandoffRecord: + return HandoffRecord.model_validate( + await self._request( + "POST", f"/v1/handoffs/{result.handoff_id}/result", payload=result + ) + ) + + async def verify(self, result: VerificationResult) -> HandoffRecord: + return HandoffRecord.model_validate( + await self._request( + "POST", f"/v1/handoffs/{result.handoff_id}/verify", payload=result + ) + ) + + async def approve(self, record: ApprovalRecord) -> HandoffRecord: + return HandoffRecord.model_validate( + await self._request( + "POST", f"/v1/approvals/{record.handoff_id}/decision", payload=record + ) + ) + + async def approvals(self, *, status: str = "awaiting_approval") -> list[HandoffRecord]: + data = await self._request("GET", "/v1/approvals", params={"status": status}) + return [HandoffRecord.model_validate(item) for item in data.get("handoffs", [])] + + async def cancel(self, handoff_id: str, reason: str = "") -> HandoffRecord: + return HandoffRecord.model_validate( + await self._request( + "POST", f"/v1/handoffs/{handoff_id}/cancel", payload={"reason": reason} + ) + ) diff --git a/agent_core/coordinator/__init__.py b/agent_core/coordinator/__init__.py new file mode 100644 index 0000000..44a30df --- /dev/null +++ b/agent_core/coordinator/__init__.py @@ -0,0 +1,5 @@ +"""Operational LHP-v2 coordinator (optional service dependency).""" + +from agent_core.coordinator.app import create_app + +__all__ = ["create_app"] diff --git a/agent_core/coordinator/__main__.py b/agent_core/coordinator/__main__.py new file mode 100644 index 0000000..6c6d8c0 --- /dev/null +++ b/agent_core/coordinator/__main__.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import os + +import uvicorn + + +def main() -> None: + uvicorn.run( + "agent_core.coordinator.app:app", + host=os.environ.get("HYRULE_COORDINATOR_HOST", "127.0.0.1"), + port=int(os.environ.get("HYRULE_COORDINATOR_PORT", "8771")), + ) + + +if __name__ == "__main__": + main() diff --git a/agent_core/coordinator/app.py b/agent_core/coordinator/app.py new file mode 100644 index 0000000..e5c33e9 --- /dev/null +++ b/agent_core/coordinator/app.py @@ -0,0 +1,496 @@ +"""FastAPI service for authoritative LHP-v2 coordination state.""" + +from __future__ import annotations + +import asyncio +import hmac +import json +import os +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from dataclasses import dataclass +from datetime import UTC, datetime +from typing import Any + +import httpx +from fastapi import Depends, FastAPI, HTTPException, Request +from pydantic import BaseModel, Field + +from agent_core import __version__ +from agent_core.contracts.coordination import ( + ApprovalRecord, + CaseProjection, + HandoffEnvelope, + HandoffRecord, + HandoffResult, + LoopHeartbeat, + LoopRegistration, + VerificationResult, +) +from agent_core.coordination.auth import build_signature +from agent_core.coordinator.db import ( + CoordinatorStore, + init_models, + make_engine, + make_sessionmaker, +) + +MAX_BODY_BYTES = 65_536 +MAX_CLOCK_SKEW_SECONDS = 300 + +_APPROVAL_RANK = {"none": 0, "operator": 1, "senior": 2, "break_glass": 3} +_MINIMUM_APPROVAL = { + "engineering.draft_pr": "operator", + "noc.network_change.prepare": "operator", + "knowledge.learning.proposal": "operator", + "soc.active_probe.rt2": "senior", +} + + +DEFAULT_REGISTRATIONS = [ + LoopRegistration( + loop_id="soc", + display_name="SOC Agent", + capabilities=[ + "security.triage", + "security.attack_path", + "security.verify", + "soc.active_probe.rt2", + ], + service_name="soc-agent.service", + host="soc", + ), + LoopRegistration( + loop_id="noc", + display_name="NOC Agent", + capabilities=[ + "noc.network_snapshot.read", + "noc.network_change.prepare", + "noc.verify", + ], + service_name="noc-agent.service", + host="noc", + ), + LoopRegistration( + loop_id="engineering", + display_name="Engineering Loop", + capabilities=["engineering.repository.analyze", "engineering.draft_pr"], + service_name="hyrule-engineering-loop.timer", + host="loop", + ), + LoopRegistration( + loop_id="knowledge", + display_name="Knowledge Loop", + capabilities=[ + "knowledge.context.resolve", + "knowledge.gap.analyze", + "knowledge.learning.proposal", + ], + service_name="hyrule-knowledge-loop.timer", + host="loop", + ), +] + + +def _load_keys() -> dict[str, dict[str, str]]: + raw = os.environ.get("HYRULE_COORDINATOR_LOOP_KEYS_JSON", "").strip() + if not raw: + return {} + try: + loaded = json.loads(raw) + except json.JSONDecodeError as exc: + raise RuntimeError("HYRULE_COORDINATOR_LOOP_KEYS_JSON must be valid JSON") from exc + if not isinstance(loaded, dict): + raise RuntimeError("HYRULE_COORDINATOR_LOOP_KEYS_JSON must be an object") + keys: dict[str, dict[str, str]] = {} + for identity, value in loaded.items(): + if isinstance(value, str): + keys[str(identity)] = {"default": value} + elif isinstance(value, dict): + keys[str(identity)] = {str(k): str(v) for k, v in value.items() if v} + return keys + + +def _truthy_env(name: str) -> bool: + return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"} + + +@dataclass +class CoordinatorAuthenticator: + keys: dict[str, dict[str, str]] + allow_insecure_dev: bool = False + + async def authenticate(self, request: Request, store: CoordinatorStore) -> str: + body = await request.body() + if len(body) > MAX_BODY_BYTES: + raise HTTPException(status_code=413, detail="coordinator payload exceeds 64 KiB") + identity = request.headers.get("x-agent-loop-identity", "").strip().lower() + if not identity: + raise HTTPException(status_code=401, detail="missing loop identity") + if self.allow_insecure_dev: + return identity + key_id = request.headers.get("x-agent-loop-key-id", "").strip() + timestamp = request.headers.get("x-agent-loop-timestamp", "").strip() + nonce = request.headers.get("x-agent-loop-nonce", "").strip() + signature = request.headers.get("x-agent-loop-signature", "").strip() + secret = self.keys.get(identity, {}).get(key_id, "") + if not secret or not timestamp or not nonce or not signature: + raise HTTPException( + status_code=401, detail="missing or unknown loop signing credentials" + ) + try: + parsed = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) + except ValueError as exc: + raise HTTPException(status_code=401, detail="invalid loop timestamp") from exc + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=UTC) + now = datetime.now(UTC) + if abs((now - parsed.astimezone(UTC)).total_seconds()) > MAX_CLOCK_SKEW_SECONDS: + raise HTTPException(status_code=401, detail="stale loop timestamp") + expected = build_signature( + secret=secret, + method=request.method, + path=request.url.path, + timestamp=timestamp, + nonce=nonce, + key_id=key_id, + body=body, + ) + if not hmac.compare_digest(expected, signature): + raise HTTPException(status_code=401, detail="invalid loop signature") + if not await store.register_nonce(identity, nonce, now): + raise HTTPException(status_code=409, detail="replayed loop nonce") + return identity + + +class LeaseRequest(BaseModel): + lease_seconds: int = Field(default=120, ge=30, le=900) + + +class ProgressRequest(BaseModel): + summary: str = Field(default="", max_length=1000) + + +class CancelRequest(BaseModel): + reason: str = Field(default="", max_length=1000) + + +async def _deliver_outbox(app: FastAPI) -> None: + collector_url = str(app.state.collector_url or "").strip() + if not collector_url: + return + store: CoordinatorStore = app.state.store + while True: + for row_id, payload in await store.outbox_batch(): + try: + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.post(collector_url, json=payload) + response.raise_for_status() + await store.mark_outbox(row_id) + except Exception as exc: + await store.mark_outbox(row_id, error=type(exc).__name__) + await asyncio.sleep(2) + + +def create_app( + *, + database_url: str | None = None, + keys: dict[str, dict[str, str]] | None = None, + allow_insecure_dev: bool | None = None, + environment: str | None = None, + registrations: list[LoopRegistration] | None = None, +) -> FastAPI: + runtime_environment = ( + environment or os.environ.get("HYRULE_COORDINATOR_ENVIRONMENT", "production") + ).strip().lower() + insecure_dev = ( + allow_insecure_dev + if allow_insecure_dev is not None + else _truthy_env("HYRULE_COORDINATOR_ALLOW_INSECURE_DEV") + ) + if insecure_dev and runtime_environment not in {"development", "test"}: + raise RuntimeError( + "HYRULE_COORDINATOR_ALLOW_INSECURE_DEV is permitted only in development/test" + ) + engine = make_engine(database_url) + store = CoordinatorStore(make_sessionmaker(engine)) + authenticator = CoordinatorAuthenticator( + keys=keys if keys is not None else _load_keys(), + allow_insecure_dev=insecure_dev, + ) + approved_registrations = registrations or DEFAULT_REGISTRATIONS + capability_map = {item.loop_id: frozenset(item.capabilities) for item in approved_registrations} + + @asynccontextmanager + async def lifespan(app: FastAPI) -> AsyncIterator[None]: + await init_models(engine) + await store.seed_loops(approved_registrations) + outbox_task = asyncio.create_task(_deliver_outbox(app)) + try: + yield + finally: + outbox_task.cancel() + try: + await outbox_task + except asyncio.CancelledError: + pass + await engine.dispose() + + app = FastAPI(title="AS215932 Agent Coordinator", version=__version__, lifespan=lifespan) + app.state.store = store + app.state.engine = engine + app.state.authenticator = authenticator + app.state.collector_url = os.environ.get("HYRULE_COORDINATOR_COLLECTOR_URL", "") + + async def identity(request: Request) -> str: + return await authenticator.authenticate(request, store) + + def require_observatory(actor: str) -> None: + if actor != "observatory": + raise HTTPException(status_code=403, detail="Observatory identity required") + + def require_visible(record: HandoffRecord, actor: str) -> None: + if actor not in { + "observatory", + record.envelope.source_loop, + record.envelope.target_loop, + }: + raise HTTPException(status_code=403, detail="handoff is not visible to this identity") + + async def record_or_404(handoff_id: str) -> HandoffRecord: + record = await store.handoff(handoff_id) + if record is None: + raise HTTPException(status_code=404, detail="handoff not found") + return record + + @app.get("/healthz") + async def healthz() -> dict[str, Any]: + return {"status": "ok", "service": "agent-core-coordinator", "version": __version__} + + @app.get("/v1/loops") + async def loops(actor: str = Depends(identity)) -> dict[str, Any]: + del actor + return {"loops": await store.loops()} + + @app.post("/v1/loops/{loop_id}/heartbeat") + async def heartbeat( + loop_id: str, payload: LoopHeartbeat, actor: str = Depends(identity) + ) -> dict[str, Any]: + if actor != loop_id or payload.loop_id != loop_id: + raise HTTPException(status_code=403, detail="loop may heartbeat only itself") + try: + return await store.heartbeat(payload) + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + @app.put("/v1/cases/{case_id}", response_model=CaseProjection) + async def put_case( + case_id: str, payload: CaseProjection, actor: str = Depends(identity) + ) -> CaseProjection: + if payload.case_id != case_id or payload.owner_loop != actor: + raise HTTPException(status_code=403, detail="case projection owner mismatch") + try: + return await store.put_case(payload) + except PermissionError as exc: + raise HTTPException(status_code=403, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + + @app.get("/v1/cases") + async def cases( + owner_loop: str | None = None, + status: str | None = None, + limit: int = 100, + actor: str = Depends(identity), + ) -> dict[str, Any]: + del actor + items = await store.cases(owner_loop=owner_loop, status=status, limit=limit) + return {"cases": [item.model_dump(mode="json") for item in items]} + + @app.get("/v1/cases/{case_id}", response_model=CaseProjection) + async def case(case_id: str, actor: str = Depends(identity)) -> CaseProjection: + del actor + item = await store.case(case_id) + if item is None: + raise HTTPException(status_code=404, detail="case not found") + return item + + @app.post("/v1/handoffs", response_model=HandoffRecord) + async def create_handoff( + payload: HandoffEnvelope, actor: str = Depends(identity) + ) -> HandoffRecord: + if payload.source_loop != actor: + raise HTTPException(status_code=403, detail="source_loop must match signed identity") + supported = capability_map.get(payload.target_loop) + if supported is None or payload.capability not in supported: + raise HTTPException(status_code=422, detail="target loop does not advertise capability") + minimum = _MINIMUM_APPROVAL.get(payload.capability, "none") + if ( + payload.risk_level in {"high", "critical"} + and payload.capability + in {"engineering.draft_pr", "noc.network_change.prepare", "knowledge.learning.proposal"} + ): + minimum = "senior" + if _APPROVAL_RANK[payload.approval_tier] < _APPROVAL_RANK[minimum]: + raise HTTPException( + status_code=422, + detail=f"{payload.capability} requires at least {minimum} approval", + ) + if payload.expires_at and payload.expires_at <= datetime.now(UTC): + raise HTTPException(status_code=422, detail="handoff is already expired") + try: + return await store.create_handoff(payload) + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + + @app.get("/v1/handoffs") + async def handoffs( + source_loop: str | None = None, + target_loop: str | None = None, + status: str | None = None, + case_id: str | None = None, + limit: int = 100, + actor: str = Depends(identity), + ) -> dict[str, Any]: + if actor != "observatory": + if source_loop and source_loop != actor: + raise HTTPException(status_code=403, detail="source filter exceeds identity scope") + if target_loop and target_loop != actor: + raise HTTPException(status_code=403, detail="target filter exceeds identity scope") + if not source_loop and not target_loop: + raise HTTPException( + status_code=422, detail="loop must select its source or target scope" + ) + items = await store.handoffs( + source_loop=source_loop, + target_loop=target_loop, + status=status, + case_id=case_id, + limit=limit, + ) + return {"handoffs": [item.model_dump(mode="json") for item in items]} + + @app.get("/v1/inbox") + async def inbox( + status: str | None = None, limit: int = 100, actor: str = Depends(identity) + ) -> dict[str, Any]: + if actor == "observatory": + raise HTTPException( + status_code=403, detail="Observatory uses the handoff/approval views" + ) + items = await store.handoffs(target_loop=actor, status=status, limit=limit) + return {"handoffs": [item.model_dump(mode="json") for item in items]} + + @app.get("/v1/handoffs/{handoff_id}", response_model=HandoffRecord) + async def handoff(handoff_id: str, actor: str = Depends(identity)) -> HandoffRecord: + record = await record_or_404(handoff_id) + require_visible(record, actor) + return record + + @app.get("/v1/handoffs/{handoff_id}/events") + async def handoff_events(handoff_id: str, actor: str = Depends(identity)) -> dict[str, Any]: + record = await record_or_404(handoff_id) + require_visible(record, actor) + events = await store.events(handoff_id) + return {"events": [event.model_dump(mode="json") for event in events]} + + @app.post("/v1/handoffs/{handoff_id}/claim", response_model=HandoffRecord) + async def claim( + handoff_id: str, payload: LeaseRequest, actor: str = Depends(identity) + ) -> HandoffRecord: + record = await record_or_404(handoff_id) + if record.envelope.target_loop != actor: + raise HTTPException(status_code=403, detail="only the target loop may claim") + try: + return await store.claim(handoff_id, actor, payload.lease_seconds) + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + + @app.post("/v1/handoffs/{handoff_id}/heartbeat", response_model=HandoffRecord) + async def heartbeat_claim( + handoff_id: str, payload: LeaseRequest, actor: str = Depends(identity) + ) -> HandoffRecord: + try: + return await store.heartbeat_claim(handoff_id, actor, payload.lease_seconds) + except (PermissionError, ValueError) as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + @app.post("/v1/handoffs/{handoff_id}/progress", response_model=HandoffRecord) + async def progress( + handoff_id: str, payload: ProgressRequest, actor: str = Depends(identity) + ) -> HandoffRecord: + try: + return await store.progress(handoff_id, actor, payload.summary) + except (PermissionError, ValueError) as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + @app.post("/v1/handoffs/{handoff_id}/result", response_model=HandoffRecord) + async def result( + handoff_id: str, payload: HandoffResult, actor: str = Depends(identity) + ) -> HandoffRecord: + try: + return await store.submit_result(handoff_id, actor, payload) + except (PermissionError, ValueError) as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + @app.post("/v1/handoffs/{handoff_id}/verify", response_model=HandoffRecord) + async def verify( + handoff_id: str, payload: VerificationResult, actor: str = Depends(identity) + ) -> HandoffRecord: + try: + return await store.verify(handoff_id, actor, payload) + except PermissionError as exc: + raise HTTPException(status_code=403, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + @app.get("/v1/approvals") + async def approvals( + status: str = "awaiting_approval", + limit: int = 100, + actor: str = Depends(identity), + ) -> dict[str, Any]: + require_observatory(actor) + items = await store.handoffs(status=status, limit=limit) + return {"handoffs": [item.model_dump(mode="json") for item in items]} + + @app.post("/v1/approvals/{handoff_id}/decision", response_model=HandoffRecord) + async def approval_decision( + handoff_id: str, payload: ApprovalRecord, actor: str = Depends(identity) + ) -> HandoffRecord: + require_observatory(actor) + try: + return await store.approve(handoff_id, payload) + except PermissionError as exc: + raise HTTPException(status_code=403, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + @app.post("/v1/handoffs/{handoff_id}/cancel", response_model=HandoffRecord) + async def cancel( + handoff_id: str, payload: CancelRequest, actor: str = Depends(identity) + ) -> HandoffRecord: + record = await record_or_404(handoff_id) + if actor not in {record.envelope.source_loop, "observatory"}: + raise HTTPException( + status_code=403, detail="only source loop or Observatory may cancel" + ) + try: + return await store.cancel(handoff_id, actor, payload.reason) + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + + return app + + +app = create_app() diff --git a/agent_core/coordinator/db.py b/agent_core/coordinator/db.py new file mode 100644 index 0000000..1f44fb3 --- /dev/null +++ b/agent_core/coordinator/db.py @@ -0,0 +1,650 @@ +"""Transactional storage for the LHP-v2 coordinator.""" + +from __future__ import annotations + +import os +from datetime import UTC, datetime +from typing import Any + +from sqlalchemy import JSON, DateTime, Integer, String, Text, UniqueConstraint, select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + async_sessionmaker, + create_async_engine, +) +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + +from agent_core.contracts._base import utcnow +from agent_core.contracts.coordination import ( + ApprovalRecord, + CaseProjection, + HandoffEnvelope, + HandoffEvent, + HandoffRecord, + HandoffResult, + HandoffStatus, + LoopHeartbeat, + LoopRegistration, + VerificationResult, + default_lease_expiry, +) + +DEFAULT_DATABASE_URL = "sqlite+aiosqlite:///./coordinator.db" + + +def database_url() -> str: + return os.environ.get("HYRULE_COORDINATOR_DATABASE_URL", DEFAULT_DATABASE_URL) + + +class Base(DeclarativeBase): + pass + + +class LoopRow(Base): + __tablename__ = "coordination_loops" + + loop_id: Mapped[str] = mapped_column(String(64), primary_key=True) + registration: Mapped[dict[str, Any]] = mapped_column(JSON) + status: Mapped[str] = mapped_column(String(32), default="unknown") + summary: Mapped[str] = mapped_column(Text, default="") + last_heartbeat_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + heartbeat: Mapped[dict[str, Any] | None] = mapped_column(JSON) + + +class CaseRow(Base): + __tablename__ = "coordination_cases" + + case_id: Mapped[str] = mapped_column(String(180), primary_key=True) + owner_loop: Mapped[str] = mapped_column(String(64), index=True) + version: Mapped[int] = mapped_column(Integer) + status: Mapped[str] = mapped_column(String(80), index=True) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) + projection: Mapped[dict[str, Any]] = mapped_column(JSON) + + +class HandoffRow(Base): + __tablename__ = "coordination_handoffs" + + handoff_id: Mapped[str] = mapped_column(String(180), primary_key=True) + source_loop: Mapped[str] = mapped_column(String(64), index=True) + target_loop: Mapped[str] = mapped_column(String(64), index=True) + capability: Mapped[str] = mapped_column(String(160), index=True) + case_id: Mapped[str] = mapped_column(String(180), default="", index=True) + status: Mapped[str] = mapped_column(String(40), index=True) + version: Mapped[int] = mapped_column(Integer, default=1) + scope_hash: Mapped[str] = mapped_column(String(64), index=True) + claim_owner: Mapped[str] = mapped_column(String(64), default="") + lease_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + envelope: Mapped[dict[str, Any]] = mapped_column(JSON) + result: Mapped[dict[str, Any] | None] = mapped_column(JSON) + verification: Mapped[dict[str, Any] | None] = mapped_column(JSON) + approval: Mapped[dict[str, Any] | None] = mapped_column(JSON) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) + expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True) + + +class HandoffEventRow(Base): + __tablename__ = "coordination_handoff_events" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + event_id: Mapped[str] = mapped_column(String(180), unique=True) + handoff_id: Mapped[str] = mapped_column(String(180), index=True) + event_type: Mapped[str] = mapped_column(String(64), index=True) + actor_id: Mapped[str] = mapped_column(String(180), index=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) + event: Mapped[dict[str, Any]] = mapped_column(JSON) + + +class IdempotencyRow(Base): + __tablename__ = "coordination_idempotency" + __table_args__ = (UniqueConstraint("source_loop", "idempotency_key"),) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + source_loop: Mapped[str] = mapped_column(String(64)) + idempotency_key: Mapped[str] = mapped_column(String(180)) + handoff_id: Mapped[str] = mapped_column(String(180), index=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + + +class NonceRow(Base): + __tablename__ = "coordination_nonces" + __table_args__ = (UniqueConstraint("loop_id", "nonce"),) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + loop_id: Mapped[str] = mapped_column(String(64)) + nonce: Mapped[str] = mapped_column(String(180)) + observed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) + + +class OutboxRow(Base): + __tablename__ = "coordination_trace_outbox" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + event_id: Mapped[str] = mapped_column(String(180), unique=True) + payload: Mapped[dict[str, Any]] = mapped_column(JSON) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) + delivered_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + last_error: Mapped[str] = mapped_column(Text, default="") + + +def make_engine(url: str | None = None) -> AsyncEngine: + return create_async_engine(url or database_url(), future=True) + + +def make_sessionmaker(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]: + return async_sessionmaker(engine, expire_on_commit=False) + + +async def init_models(engine: AsyncEngine) -> None: + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +def _aware(value: datetime | None) -> datetime | None: + if value is None or value.tzinfo is not None: + return value + return value.replace(tzinfo=UTC) + + +class CoordinatorStore: + def __init__(self, sessionmaker: async_sessionmaker[AsyncSession]) -> None: + self.sessions = sessionmaker + + async def register_nonce(self, loop_id: str, nonce: str, observed_at: datetime) -> bool: + async with self.sessions() as session: + session.add(NonceRow(loop_id=loop_id, nonce=nonce, observed_at=observed_at)) + try: + await session.commit() + return True + except IntegrityError: + await session.rollback() + return False + + async def seed_loops(self, registrations: list[LoopRegistration]) -> None: + async with self.sessions() as session, session.begin(): + for registration in registrations: + row = await session.get(LoopRow, registration.loop_id) + payload = registration.model_dump(mode="json") + if row is None: + session.add( + LoopRow( + loop_id=registration.loop_id, + registration=payload, + status="disabled" if not registration.enabled else "unknown", + summary="", + ) + ) + else: + row.registration = payload + + async def loops(self) -> list[dict[str, Any]]: + async with self.sessions() as session: + rows = (await session.execute(select(LoopRow).order_by(LoopRow.loop_id))).scalars() + return [ + { + **row.registration, + "status": row.status, + "summary": row.summary, + "last_heartbeat_at": _aware(row.last_heartbeat_at), + "heartbeat": row.heartbeat, + } + for row in rows + ] + + async def heartbeat(self, heartbeat: LoopHeartbeat) -> dict[str, Any]: + async with self.sessions() as session, session.begin(): + row = await session.get(LoopRow, heartbeat.loop_id) + if row is None: + raise KeyError("loop is not registered") + row.status = heartbeat.status + row.summary = heartbeat.summary + row.last_heartbeat_at = heartbeat.observed_at + row.heartbeat = heartbeat.model_dump(mode="json") + return {**row.registration, **heartbeat.model_dump(mode="json")} + + async def put_case(self, projection: CaseProjection) -> CaseProjection: + async with self.sessions() as session, session.begin(): + row = await session.get(CaseRow, projection.case_id, with_for_update=True) + if row is not None: + if row.owner_loop != projection.owner_loop: + raise PermissionError("case owner cannot change") + if projection.version < row.version: + raise ValueError("stale case projection version") + row.version = projection.version + row.status = projection.status + row.updated_at = projection.updated_at + row.projection = projection.model_dump(mode="json") + else: + session.add( + CaseRow( + case_id=projection.case_id, + owner_loop=projection.owner_loop, + version=projection.version, + status=projection.status, + updated_at=projection.updated_at, + projection=projection.model_dump(mode="json"), + ) + ) + return projection + + async def cases( + self, *, owner_loop: str | None = None, status: str | None = None, limit: int = 100 + ) -> list[CaseProjection]: + statement = select(CaseRow) + if owner_loop: + statement = statement.where(CaseRow.owner_loop == owner_loop) + if status: + statement = statement.where(CaseRow.status == status) + statement = statement.order_by(CaseRow.updated_at.desc()).limit(max(1, min(limit, 500))) + async with self.sessions() as session: + rows = (await session.execute(statement)).scalars() + return [CaseProjection.model_validate(row.projection) for row in rows] + + async def case(self, case_id: str) -> CaseProjection | None: + async with self.sessions() as session: + row = await session.get(CaseRow, case_id) + return CaseProjection.model_validate(row.projection) if row else None + + async def create_handoff(self, envelope: HandoffEnvelope) -> HandoffRecord: + async with self.sessions() as session, session.begin(): + existing_id = ( + await session.execute( + select(IdempotencyRow.handoff_id).where( + IdempotencyRow.source_loop == envelope.source_loop, + IdempotencyRow.idempotency_key == envelope.idempotency_key, + ) + ) + ).scalar_one_or_none() + if existing_id: + row = await session.get(HandoffRow, existing_id) + if row is None: + raise RuntimeError("idempotency record references a missing handoff") + return self._record(row) + if await session.get(HandoffRow, envelope.handoff_id) is not None: + raise ValueError("handoff_id already exists") + status: HandoffStatus = ( + "queued" if envelope.approval_tier == "none" else "awaiting_approval" + ) + now = utcnow() + row = HandoffRow( + handoff_id=envelope.handoff_id, + source_loop=envelope.source_loop, + target_loop=envelope.target_loop, + capability=envelope.capability, + case_id=envelope.case_id, + status=status, + version=1, + scope_hash=envelope.scope_hash, + claim_owner="", + envelope=envelope.model_dump(mode="json"), + created_at=envelope.created_at, + updated_at=now, + expires_at=envelope.expires_at, + ) + session.add(row) + session.add( + IdempotencyRow( + source_loop=envelope.source_loop, + idempotency_key=envelope.idempotency_key, + handoff_id=envelope.handoff_id, + created_at=now, + ) + ) + self._add_event( + session, + row, + event_type="created", + actor_id=envelope.source_loop, + actor_loop=envelope.source_loop, + from_status=None, + summary=envelope.summary, + ) + if status == "awaiting_approval": + self._add_event( + session, + row, + event_type="approval_requested", + actor_id="coordinator", + actor_loop="", + from_status=status, + summary=f"{envelope.approval_tier} approval required", + ) + return self._record(row) + + async def handoff(self, handoff_id: str) -> HandoffRecord | None: + async with self.sessions() as session: + row = await session.get(HandoffRow, handoff_id) + return self._record(row) if row else None + + async def handoffs( + self, + *, + source_loop: str | None = None, + target_loop: str | None = None, + status: str | None = None, + case_id: str | None = None, + limit: int = 100, + ) -> list[HandoffRecord]: + statement = select(HandoffRow) + if source_loop: + statement = statement.where(HandoffRow.source_loop == source_loop) + if target_loop: + statement = statement.where(HandoffRow.target_loop == target_loop) + if status: + statement = statement.where(HandoffRow.status == status) + if case_id: + statement = statement.where(HandoffRow.case_id == case_id) + statement = statement.order_by(HandoffRow.updated_at.desc()).limit(max(1, min(limit, 500))) + async with self.sessions() as session: + rows = (await session.execute(statement)).scalars() + return [self._record(row) for row in rows] + + async def events(self, handoff_id: str) -> list[HandoffEvent]: + async with self.sessions() as session: + rows = ( + await session.execute( + select(HandoffEventRow) + .where(HandoffEventRow.handoff_id == handoff_id) + .order_by(HandoffEventRow.id) + ) + ).scalars() + return [HandoffEvent.model_validate(row.event) for row in rows] + + async def outbox_batch(self, limit: int = 100) -> list[tuple[int, dict[str, Any]]]: + async with self.sessions() as session: + rows = ( + await session.execute( + select(OutboxRow) + .where(OutboxRow.delivered_at.is_(None)) + .order_by(OutboxRow.id) + .limit(max(1, min(limit, 500))) + ) + ).scalars() + return [(row.id, dict(row.payload)) for row in rows] + + async def mark_outbox(self, row_id: int, *, error: str = "") -> None: + async with self.sessions() as session, session.begin(): + row = await session.get(OutboxRow, row_id, with_for_update=True) + if row is None: + return + if error: + row.last_error = error[:1000] + else: + row.delivered_at = utcnow() + row.last_error = "" + + async def approve(self, handoff_id: str, approval: ApprovalRecord) -> HandoffRecord: + async with self.sessions() as session, session.begin(): + row = await self._locked(session, handoff_id) + if row.status != "awaiting_approval": + raise ValueError("handoff is not awaiting approval") + if approval.handoff_id != row.handoff_id or approval.scope_hash != row.scope_hash: + raise ValueError("approval does not match the handoff scope") + envelope = HandoffEnvelope.model_validate(row.envelope) + if envelope.approval_tier == "senior" and approval.approver_role != "senior": + raise PermissionError("senior approval is required") + if approval.expires_at and approval.expires_at <= utcnow(): + raise ValueError("approval has expired") + previous = row.status + row.approval = approval.model_dump(mode="json") + row.status = "queued" if approval.decision == "approved" else "rejected" + self._touch(row) + self._add_event( + session, + row, + event_type="approved" if approval.decision == "approved" else "rejected", + actor_id=approval.approver_id, + actor_loop="observatory", + from_status=previous, + summary=approval.rationale, + ) + return self._record(row) + + async def claim(self, handoff_id: str, actor_loop: str, lease_seconds: int) -> HandoffRecord: + async with self.sessions() as session, session.begin(): + row = await self._locked(session, handoff_id) + now = utcnow() + aware_lease = _aware(row.lease_expires_at) + lease_expired = aware_lease is not None and aware_lease <= now + if row.status not in {"queued", "claimed", "in_progress"}: + raise ValueError("handoff is not claimable") + if row.approval: + approval = ApprovalRecord.model_validate(row.approval) + if approval.scope_hash != row.scope_hash: + raise ValueError("stored approval no longer matches handoff scope") + if approval.expires_at and approval.expires_at <= now: + raise ValueError("handoff approval has expired") + if row.status != "queued" and not lease_expired and row.claim_owner != actor_loop: + raise ValueError("handoff is leased by another worker") + previous = row.status + row.status = "claimed" + row.claim_owner = actor_loop + row.lease_expires_at = default_lease_expiry(lease_seconds) + self._touch(row) + self._add_event( + session, + row, + event_type="claimed", + actor_id=actor_loop, + actor_loop=actor_loop, + from_status=previous, + summary="handoff claimed", + ) + return self._record(row) + + async def heartbeat_claim( + self, handoff_id: str, actor_loop: str, lease_seconds: int + ) -> HandoffRecord: + async with self.sessions() as session, session.begin(): + row = await self._locked(session, handoff_id) + if row.claim_owner != actor_loop or row.status not in {"claimed", "in_progress"}: + raise PermissionError("loop does not own the active claim") + row.lease_expires_at = default_lease_expiry(lease_seconds) + self._touch(row) + self._add_event( + session, + row, + event_type="heartbeat", + actor_id=actor_loop, + actor_loop=actor_loop, + from_status=row.status, + summary="claim lease extended", + ) + return self._record(row) + + async def progress(self, handoff_id: str, actor_loop: str, summary: str) -> HandoffRecord: + async with self.sessions() as session, session.begin(): + row = await self._locked(session, handoff_id) + if row.claim_owner != actor_loop or row.status not in {"claimed", "in_progress"}: + raise PermissionError("loop does not own the active claim") + previous = row.status + row.status = "in_progress" + self._touch(row) + self._add_event( + session, + row, + event_type="progress", + actor_id=actor_loop, + actor_loop=actor_loop, + from_status=previous, + summary=summary, + ) + return self._record(row) + + async def submit_result( + self, handoff_id: str, actor_loop: str, result: HandoffResult + ) -> HandoffRecord: + async with self.sessions() as session, session.begin(): + row = await self._locked(session, handoff_id) + if row.claim_owner != actor_loop or row.status not in {"claimed", "in_progress"}: + raise PermissionError("loop does not own the active claim") + if result.handoff_id != handoff_id: + raise ValueError("result handoff_id mismatch") + previous = row.status + row.result = result.model_dump(mode="json") + row.lease_expires_at = None + row.status = ( + "failed" + if result.outcome in {"failed", "rejected"} + else "result_submitted" + ) + self._touch(row) + self._add_event( + session, + row, + event_type="failed" if row.status == "failed" else "result_submitted", + actor_id=actor_loop, + actor_loop=actor_loop, + from_status=previous, + summary=result.summary, + ) + return self._record(row) + + async def verify( + self, handoff_id: str, actor_loop: str, verification: VerificationResult + ) -> HandoffRecord: + async with self.sessions() as session, session.begin(): + row = await self._locked(session, handoff_id) + if row.source_loop != actor_loop: + raise PermissionError("only the source loop may verify a handoff") + if row.status not in {"result_submitted", "verification_pending"}: + raise ValueError("handoff has no result awaiting verification") + if verification.handoff_id != handoff_id: + raise ValueError("verification handoff_id mismatch") + previous = row.status + row.verification = verification.model_dump(mode="json") + if verification.verdict == "pending": + row.status = "verification_pending" + event_type = "verification_pending" + elif verification.verdict == "passed": + row.status = "completed" + event_type = "verified" + else: + row.status = "failed" + event_type = "failed" + self._touch(row) + self._add_event( + session, + row, + event_type=event_type, + actor_id=actor_loop, + actor_loop=actor_loop, + from_status=previous, + summary=verification.summary, + ) + return self._record(row) + + async def cancel(self, handoff_id: str, actor_id: str, reason: str) -> HandoffRecord: + async with self.sessions() as session, session.begin(): + row = await self._locked(session, handoff_id) + if row.status in {"completed", "rejected", "failed", "cancelled", "expired"}: + raise ValueError("terminal handoff cannot be cancelled") + previous = row.status + row.status = "cancelled" + row.lease_expires_at = None + self._touch(row) + self._add_event( + session, + row, + event_type="cancelled", + actor_id=actor_id, + actor_loop=actor_id if actor_id != "observatory" else "", + from_status=previous, + summary=reason, + ) + return self._record(row) + + async def _locked(self, session: AsyncSession, handoff_id: str) -> HandoffRow: + row = ( + await session.execute( + select(HandoffRow) + .where(HandoffRow.handoff_id == handoff_id) + .with_for_update() + ) + ).scalar_one_or_none() + if row is None: + raise KeyError("handoff not found") + return row + + @staticmethod + def _touch(row: HandoffRow) -> None: + row.version += 1 + row.updated_at = utcnow() + + @staticmethod + def _record(row: HandoffRow) -> HandoffRecord: + return HandoffRecord( + envelope=HandoffEnvelope.model_validate(row.envelope), + status=row.status, # type: ignore[arg-type] + version=row.version, + claim_owner=row.claim_owner, + lease_expires_at=_aware(row.lease_expires_at), + result=HandoffResult.model_validate(row.result) if row.result else None, + verification=( + VerificationResult.model_validate(row.verification) if row.verification else None + ), + approval=ApprovalRecord.model_validate(row.approval) if row.approval else None, + updated_at=_aware(row.updated_at) or utcnow(), + ) + + @staticmethod + def _add_event( + session: AsyncSession, + row: HandoffRow, + *, + event_type: str, + actor_id: str, + actor_loop: str, + from_status: str | None, + summary: str, + ) -> None: + event = HandoffEvent( + handoff_id=row.handoff_id, + event_type=event_type, # type: ignore[arg-type] + actor_id=actor_id, + actor_loop=actor_loop, + from_status=from_status, # type: ignore[arg-type] + to_status=row.status, # type: ignore[arg-type] + handoff_version=row.version, + summary=summary, + ) + session.add( + HandoffEventRow( + event_id=event.event_id, + handoff_id=row.handoff_id, + event_type=event.event_type, + actor_id=event.actor_id, + created_at=event.created_at, + event=event.model_dump(mode="json"), + ) + ) + session.add( + OutboxRow( + event_id=event.event_id, + payload={ + "event_type": "handoff_transition", + # Collector traces are structural telemetry only. The full + # event remains in the coordinator audit table; never copy + # caller-controlled summaries, rationale, results, or + # payloads into the shared trace stream. + "summary": f"{row.handoff_id}: {event.event_type}", + "handoff_id": row.handoff_id, + "case_id": row.case_id, + "payload": { + "event_id": event.event_id, + "event_type": event.event_type, + "actor_loop": event.actor_loop, + "from_status": event.from_status, + "to_status": event.to_status, + "handoff_version": event.handoff_version, + "source_loop": row.source_loop, + "target_loop": row.target_loop, + "capability": row.capability, + }, + }, + created_at=event.created_at, + ) + ) diff --git a/agent_core/py.typed b/agent_core/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/agent_core/py.typed @@ -0,0 +1 @@ + diff --git a/pyproject.toml b/pyproject.toml index c6aee37..9e55327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,21 +4,26 @@ build-backend = "hatchling.build" [project] name = "agent-core" -version = "0.7.0" -description = "Shared typed contracts for the AS215932 Agent Runtime Framework (Phase 1 / §31 safe milestone)." +version = "0.9.0" +description = "Shared typed contracts and optional coordination services for AS215932 agentic loops." requires-python = ">=3.11" dependencies = ["pydantic>=2,<3"] [project.optional-dependencies] collector = ["fastapi>=0.110", "uvicorn>=0.29", "sqlalchemy>=2", "asyncpg>=0.29"] +coordination-client = ["httpx>=0.27"] +coordinator = [ + "fastapi>=0.110", "uvicorn>=0.29", "sqlalchemy>=2", "asyncpg>=0.29", + "httpx>=0.27", "aiosqlite>=0.20", +] dev = [ - "pytest>=8", "pyyaml>=6", "ruff>=0.5", "mypy>=1.8", "types-PyYAML", + "pytest>=8", "pytest-asyncio>=0.23", "pyyaml>=6", "ruff>=0.5", "mypy>=1.8", "types-PyYAML", "fastapi>=0.110", "uvicorn>=0.29", "sqlalchemy>=2", "httpx>=0.27", "aiosqlite>=0.20", ] [dependency-groups] dev = [ - "pytest>=8", "pyyaml>=6", "ruff>=0.5", "mypy>=1.8", "types-PyYAML", + "pytest>=8", "pytest-asyncio>=0.23", "pyyaml>=6", "ruff>=0.5", "mypy>=1.8", "types-PyYAML", "fastapi>=0.110", "uvicorn>=0.29", "sqlalchemy>=2", "httpx>=0.27", "aiosqlite>=0.20", ] @@ -41,3 +46,7 @@ disallow_untyped_defs = true [tool.pytest.ini_options] testpaths = ["tests"] +asyncio_mode = "auto" + +[project.scripts] +agent-core-coordinator = "agent_core.coordinator.__main__:main" diff --git a/tests/contracts/test_coordination_contracts.py b/tests/contracts/test_coordination_contracts.py new file mode 100644 index 0000000..2c090e3 --- /dev/null +++ b/tests/contracts/test_coordination_contracts.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from datetime import timedelta + +import pytest +from pydantic import ValidationError + +from agent_core.contracts import ( + ApprovalRecord, + HandoffEnvelope, + ProbePlan, + SourceRef, + utcnow, +) + + +def test_handoff_scope_hash_is_stable_and_detects_mutation() -> None: + envelope = HandoffEnvelope( + source_loop="soc", + target_loop="engineering", + capability="engineering.draft_pr", + summary="Draft a bounded configuration change", + risk_level="medium", + approval_tier="operator", + payload={"repository": "AS215932/network-operations", "paths": ["docs/"]}, + idempotency_key="soc:case-1:engineering", + ) + assert len(envelope.scope_hash) == 64 + + dumped = envelope.model_dump(mode="json") + assert HandoffEnvelope.model_validate(dumped).scope_hash == envelope.scope_hash + dumped["payload"]["paths"] = ["ansible/"] + with pytest.raises(ValidationError, match="scope_hash"): + HandoffEnvelope.model_validate(dumped) + + +def test_probe_plan_enforces_rt2_bounds() -> None: + plan = ProbePlan( + probe_kind="tcp_connect_sweep", + targets=["web.as215932.net"], + ports=[80, 443], + approved_asset_refs=[SourceRef(ref="okf:asset:web", authority="A1")], + ) + assert plan.max_concurrency == 2 + assert plan.max_requests == 100 + + with pytest.raises(ValidationError): + ProbePlan( + probe_kind="tcp_connect_sweep", + targets=["one", "two", "three", "four"], + ports=[443], + approved_asset_refs=[SourceRef(ref="okf:asset:web", authority="A1")], + ) + + +def test_approval_is_bound_to_scope_and_can_expire() -> None: + envelope = HandoffEnvelope( + source_loop="soc", + target_loop="soc", + capability="soc.active_probe.rt2", + approval_tier="senior", + payload={"probe_kind": "tls_handshake"}, + idempotency_key="probe:one", + ) + approval = ApprovalRecord( + handoff_id=envelope.handoff_id, + scope_hash=envelope.scope_hash, + decision="approved", + approver_id="123", + approver_login="operator", + approver_role="senior", + expires_at=utcnow() + timedelta(minutes=15), + ) + assert approval.scope_hash == envelope.scope_hash diff --git a/tests/coordinator/test_coordinator.py b/tests/coordinator/test_coordinator.py new file mode 100644 index 0000000..0154c03 --- /dev/null +++ b/tests/coordinator/test_coordinator.py @@ -0,0 +1,236 @@ +from __future__ import annotations + +import json +from datetime import timedelta + +import httpx +import pytest + +from agent_core.contracts import ( + ApprovalRecord, + CaseProjection, + HandoffEnvelope, + HandoffResult, + VerificationResult, + utcnow, +) +from agent_core.coordination import CoordinatorClient, LoopRequestSigner +from agent_core.coordinator.app import create_app + +KEYS = { + "soc": {"v1": "soc-secret-at-least-32-characters-long"}, + "knowledge": {"v1": "knowledge-secret-at-least-32-chars"}, + "engineering": {"v1": "engineering-secret-at-least-32-char"}, + "noc": {"v1": "noc-secret-at-least-32-characters-long"}, + "observatory": {"v1": "observatory-secret-at-least-32-chars"}, +} + + +def _client(app: object, loop_id: str) -> CoordinatorClient: + return CoordinatorClient( + "http://coordinator", + signer=LoopRequestSigner(loop_id, "v1", KEYS[loop_id]["v1"]), + transport=httpx.ASGITransport(app=app), # type: ignore[arg-type] + ) + + +@pytest.mark.asyncio +async def test_full_handoff_lifecycle_and_case_projection(tmp_path) -> None: + app = create_app(database_url=f"sqlite+aiosqlite:///{tmp_path / 'coordinator.db'}", keys=KEYS) + async with app.router.lifespan_context(app): + soc = _client(app, "soc") + knowledge = _client(app, "knowledge") + observatory = _client(app, "observatory") + + projection = await soc.put_case( + CaseProjection( + case_id="soc_case_1", + owner_loop="soc", + title="Posture drift", + summary="Sanitized case projection", + ) + ) + assert projection.owner_loop == "soc" + assert (await knowledge.cases(owner_loop="soc"))[0].case_id == "soc_case_1" + assert (await observatory.case("soc_case_1")).title == "Posture drift" + + envelope = HandoffEnvelope( + source_loop="soc", + target_loop="knowledge", + capability="knowledge.context.resolve", + case_id="soc_case_1", + summary="Resolve governed context", + payload={"query": "expected TLS policy"}, + idempotency_key="soc_case_1:knowledge-context", + ) + created = await soc.create_handoff(envelope) + assert created.status == "queued" + assert (await observatory.handoffs(case_id="soc_case_1"))[0].status == "queued" + assert (await observatory.handoff_events(envelope.handoff_id))[0]["event_type"] == "created" + + duplicate = await soc.create_handoff(envelope) + assert duplicate.envelope.handoff_id == created.envelope.handoff_id + + inbox = await knowledge.inbox(status="queued") + assert [item.envelope.handoff_id for item in inbox] == [envelope.handoff_id] + claimed = await knowledge.claim(envelope.handoff_id) + assert claimed.status == "claimed" + progress = await knowledge.progress(envelope.handoff_id, "context resolved") + assert progress.status == "in_progress" + submitted = await knowledge.submit_result( + HandoffResult( + handoff_id=envelope.handoff_id, + outcome="succeeded", + summary="Returned cited context", + payload={"context_pack_id": "ctx_1"}, + ) + ) + assert submitted.status == "result_submitted" + verified = await soc.verify( + VerificationResult( + handoff_id=envelope.handoff_id, + verdict="passed", + summary="Context was consumed", + ) + ) + assert verified.status == "completed" + + +@pytest.mark.asyncio +async def test_senior_approval_and_replay_protection(tmp_path) -> None: + app = create_app(database_url=f"sqlite+aiosqlite:///{tmp_path / 'coordinator.db'}", keys=KEYS) + async with app.router.lifespan_context(app): + soc = _client(app, "soc") + observatory = _client(app, "observatory") + envelope = HandoffEnvelope( + source_loop="soc", + target_loop="soc", + capability="soc.active_probe.rt2", + approval_tier="senior", + risk_level="high", + payload={"probe_kind": "tls_handshake", "targets": ["web.as215932.net"]}, + idempotency_key="probe:tls:web", + ) + created = await soc.create_handoff(envelope) + assert created.status == "awaiting_approval" + assert len(await observatory.approvals()) == 1 + + with pytest.raises(Exception, match="senior approval"): + await observatory.approve( + ApprovalRecord( + handoff_id=envelope.handoff_id, + scope_hash=envelope.scope_hash, + decision="approved", + approver_id="100", + approver_role="operator", + ) + ) + + approved = await observatory.approve( + ApprovalRecord( + handoff_id=envelope.handoff_id, + scope_hash=envelope.scope_hash, + decision="approved", + approver_id="101", + approver_login="owner", + approver_role="senior", + expires_at=utcnow() + timedelta(minutes=15), + ) + ) + assert approved.status == "queued" + assert (await soc.claim(envelope.handoff_id)).status == "claimed" + + signer = LoopRequestSigner("soc", "v1", KEYS["soc"]["v1"]) + headers = signer.headers(method="GET", path="/v1/loops") + async with httpx.AsyncClient( + base_url="http://coordinator", transport=httpx.ASGITransport(app=app) + ) as raw: + assert (await raw.get("/v1/loops", headers=headers)).status_code == 200 + replay = await raw.get("/v1/loops", headers=headers) + assert replay.status_code == 409 + assert replay.json()["detail"] == "replayed loop nonce" + + +@pytest.mark.asyncio +async def test_signature_covers_body(tmp_path) -> None: + app = create_app(database_url=f"sqlite+aiosqlite:///{tmp_path / 'coordinator.db'}", keys=KEYS) + async with app.router.lifespan_context(app): + body = json.dumps({"loop_id": "soc", "status": "active"}).encode() + signer = LoopRequestSigner("soc", "v1", KEYS["soc"]["v1"]) + headers = signer.headers(method="POST", path="/v1/loops/soc/heartbeat", body=body) + tampered = json.dumps({"loop_id": "soc", "status": "degraded"}).encode() + async with httpx.AsyncClient( + base_url="http://coordinator", transport=httpx.ASGITransport(app=app) + ) as raw: + response = await raw.post( + "/v1/loops/soc/heartbeat", + content=tampered, + headers={**headers, "Content-Type": "application/json"}, + ) + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_capability_policy_cannot_be_downgraded_by_source(tmp_path) -> None: + app = create_app(database_url=f"sqlite+aiosqlite:///{tmp_path / 'coordinator.db'}", keys=KEYS) + async with app.router.lifespan_context(app): + soc = _client(app, "soc") + weak = HandoffEnvelope( + source_loop="soc", + target_loop="engineering", + capability="engineering.draft_pr", + approval_tier="none", + payload={"repository": "AS215932/network-operations"}, + idempotency_key="weak-engineering-scope", + ) + with pytest.raises(Exception, match="requires at least operator approval"): + await soc.create_handoff(weak) + + weak_probe = HandoffEnvelope( + source_loop="soc", + target_loop="soc", + capability="soc.active_probe.rt2", + approval_tier="operator", + payload={"probe_kind": "tls_handshake"}, + idempotency_key="weak-probe-scope", + ) + with pytest.raises(Exception, match="requires at least senior approval"): + await soc.create_handoff(weak_probe) + + +def test_insecure_auth_cannot_be_enabled_in_production(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("HYRULE_COORDINATOR_ALLOW_INSECURE_DEV", "true") + monkeypatch.delenv("HYRULE_COORDINATOR_ENVIRONMENT", raising=False) + with pytest.raises(RuntimeError, match="only in development/test"): + create_app(database_url=f"sqlite+aiosqlite:///{tmp_path / 'blocked.db'}") + + app = create_app( + database_url=f"sqlite+aiosqlite:///{tmp_path / 'test.db'}", + environment="test", + ) + assert app.state.authenticator.allow_insecure_dev is True + + +@pytest.mark.asyncio +async def test_trace_outbox_contains_only_structural_handoff_metadata(tmp_path) -> None: + app = create_app( + database_url=f"sqlite+aiosqlite:///{tmp_path / 'coordinator.db'}", keys=KEYS + ) + async with app.router.lifespan_context(app): + soc = _client(app, "soc") + envelope = HandoffEnvelope( + source_loop="soc", + target_loop="knowledge", + capability="knowledge.context.resolve", + summary="authorization: bearer do-not-export", + payload={"query": "private raw telemetry do-not-export"}, + idempotency_key="trace-redaction", + ) + await soc.create_handoff(envelope) + batch = await app.state.store.outbox_batch() + + encoded = json.dumps([payload for _row_id, payload in batch], sort_keys=True) + assert "do-not-export" not in encoded + assert "handoff_event" not in encoded + assert '"event_type": "created"' in encoded + assert '"source_loop": "soc"' in encoded diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d745e42 --- /dev/null +++ b/uv.lock @@ -0,0 +1,900 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "agent-core" +version = "0.9.0" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, +] + +[package.optional-dependencies] +collector = [ + { name = "asyncpg" }, + { name = "fastapi" }, + { name = "sqlalchemy" }, + { name = "uvicorn" }, +] +coordination-client = [ + { name = "httpx" }, +] +coordinator = [ + { name = "aiosqlite" }, + { name = "asyncpg" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "sqlalchemy" }, + { name = "uvicorn" }, +] +dev = [ + { name = "aiosqlite" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pyyaml" }, + { name = "ruff" }, + { name = "sqlalchemy" }, + { name = "types-pyyaml" }, + { name = "uvicorn" }, +] + +[package.dev-dependencies] +dev = [ + { name = "aiosqlite" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pyyaml" }, + { name = "ruff" }, + { name = "sqlalchemy" }, + { name = "types-pyyaml" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiosqlite", marker = "extra == 'coordinator'", specifier = ">=0.20" }, + { name = "aiosqlite", marker = "extra == 'dev'", specifier = ">=0.20" }, + { name = "asyncpg", marker = "extra == 'collector'", specifier = ">=0.29" }, + { name = "asyncpg", marker = "extra == 'coordinator'", specifier = ">=0.29" }, + { name = "fastapi", marker = "extra == 'collector'", specifier = ">=0.110" }, + { name = "fastapi", marker = "extra == 'coordinator'", specifier = ">=0.110" }, + { name = "fastapi", marker = "extra == 'dev'", specifier = ">=0.110" }, + { name = "httpx", marker = "extra == 'coordination-client'", specifier = ">=0.27" }, + { name = "httpx", marker = "extra == 'coordinator'", specifier = ">=0.27" }, + { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8" }, + { name = "pydantic", specifier = ">=2,<3" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, + { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5" }, + { name = "sqlalchemy", marker = "extra == 'collector'", specifier = ">=2" }, + { name = "sqlalchemy", marker = "extra == 'coordinator'", specifier = ">=2" }, + { name = "sqlalchemy", marker = "extra == 'dev'", specifier = ">=2" }, + { name = "types-pyyaml", marker = "extra == 'dev'" }, + { name = "uvicorn", marker = "extra == 'collector'", specifier = ">=0.29" }, + { name = "uvicorn", marker = "extra == 'coordinator'", specifier = ">=0.29" }, + { name = "uvicorn", marker = "extra == 'dev'", specifier = ">=0.29" }, +] +provides-extras = ["collector", "coordination-client", "coordinator", "dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "aiosqlite", specifier = ">=0.20" }, + { name = "fastapi", specifier = ">=0.110" }, + { name = "httpx", specifier = ">=0.27" }, + { name = "mypy", specifier = ">=1.8" }, + { name = "pytest", specifier = ">=8" }, + { name = "pytest-asyncio", specifier = ">=0.23" }, + { name = "pyyaml", specifier = ">=6" }, + { name = "ruff", specifier = ">=0.5" }, + { name = "sqlalchemy", specifier = ">=2" }, + { name = "types-pyyaml" }, + { name = "uvicorn", specifier = ">=0.29" }, +] + +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" }, + { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" }, + { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" }, + { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" }, + { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" }, + { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, +] + +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/17/cc02bc49bc350623d050fa139e34ea512cd6e020562f2a7312a7bcae4bc9/asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d", size = 643159, upload-time = "2025-11-24T23:25:36.443Z" }, + { url = "https://files.pythonhosted.org/packages/a4/62/4ded7d400a7b651adf06f49ea8f73100cca07c6df012119594d1e3447aa6/asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab", size = 638157, upload-time = "2025-11-24T23:25:37.89Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/4179538a9a72166a0bf60ad783b1ef16efb7960e4d7b9afe9f77a5551680/asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c", size = 2918051, upload-time = "2025-11-24T23:25:39.461Z" }, + { url = "https://files.pythonhosted.org/packages/e6/35/c27719ae0536c5b6e61e4701391ffe435ef59539e9360959240d6e47c8c8/asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109", size = 2972640, upload-time = "2025-11-24T23:25:41.512Z" }, + { url = "https://files.pythonhosted.org/packages/43/f4/01ebb9207f29e645a64699b9ce0eefeff8e7a33494e1d29bb53736f7766b/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da", size = 2851050, upload-time = "2025-11-24T23:25:43.153Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f4/03ff1426acc87be0f4e8d40fa2bff5c3952bef0080062af9efc2212e3be8/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9", size = 2962574, upload-time = "2025-11-24T23:25:44.942Z" }, + { url = "https://files.pythonhosted.org/packages/c7/39/cc788dfca3d4060f9d93e67be396ceec458dfc429e26139059e58c2c244d/asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24", size = 521076, upload-time = "2025-11-24T23:25:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/28/fc/735af5384c029eb7f1ca60ccb8fa95521dbdaeef788edf4cecfc604c3cab/asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047", size = 584980, upload-time = "2025-11-24T23:25:47.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, + { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, + { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, + { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.139.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424, upload-time = "2026-06-26T18:20:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523, upload-time = "2026-06-26T19:07:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315, upload-time = "2026-06-26T19:10:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381, upload-time = "2026-06-26T18:32:16.077Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771, upload-time = "2026-06-26T19:09:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048, upload-time = "2026-06-26T18:31:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137, upload-time = "2026-06-26T18:23:21.664Z" }, + { url = "https://files.pythonhosted.org/packages/fb/96/b9820295576ef18c9edc404f10e260ae7215ceaf3781a54b720ed2627862/greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44", size = 237630, upload-time = "2026-06-26T18:24:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, + { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, + { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, + { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, + { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211, upload-time = "2026-06-26T18:22:37.671Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, + { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, + { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, + { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, + { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, + { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, + { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, + { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, + { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/25/a6498964cfeec270c468cffdc118f69c29b412593610d55fa1327ca51ff4/librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082", size = 148029, upload-time = "2026-07-08T12:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/dc86d1bffd8e0c2818bace29d9f7783cfbb8e0673bf3673b5bbd5bbe0420/librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14", size = 153036, upload-time = "2026-07-08T12:24:47.257Z" }, + { url = "https://files.pythonhosted.org/packages/29/3f/b923826660f02f286186cd9303d52bb05ced0a13708edc104dc8480920e3/librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79", size = 493062, upload-time = "2026-07-08T12:24:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/6c0980a9c9b1302cb68d108906697b89eceb55889bb1dcf77c109aa56ca5/librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176", size = 485510, upload-time = "2026-07-08T12:24:49.727Z" }, + { url = "https://files.pythonhosted.org/packages/32/81/795ae3b9df5dd94079fb807e38191855e023e8c6249014ae6bc3f0d9a490/librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89", size = 515909, upload-time = "2026-07-08T12:24:51.135Z" }, + { url = "https://files.pythonhosted.org/packages/20/e5/182de15abce8907108a6fdb41487de65beb5099b74dc5841b19b099168db/librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f", size = 508620, upload-time = "2026-07-08T12:24:52.358Z" }, + { url = "https://files.pythonhosted.org/packages/32/03/33978d32db76e1f66377e8f78e42a2ca3c162143331677d1f50bbad36cfb/librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d", size = 530363, upload-time = "2026-07-08T12:24:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f5/b291fbd2d00f7d8287bcbf67b5aa0c6afed4bc26cef23e079629c47a2c04/librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd", size = 534209, upload-time = "2026-07-08T12:24:55.138Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/6f41f17939d191bc21609f220da8509316bc62797f078545fe83be522e78/librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588", size = 514254, upload-time = "2026-07-08T12:24:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/af/c2/2e4befa5410a7443019c14abccc94ff619797171f6b72013635fb87f31d7/librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1", size = 557611, upload-time = "2026-07-08T12:24:57.561Z" }, + { url = "https://files.pythonhosted.org/packages/ab/54/8b69f81448417adbc040a2185f4e2eece1e1994b7dcfaeed4662b30f98a5/librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21", size = 104906, upload-time = "2026-07-08T12:24:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/76/5a/f4aaf37b50f2fde12c8c663b83fdd499cdc24f957f19543d7414bfcc9e25/librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b", size = 125852, upload-time = "2026-07-08T12:25:00.065Z" }, + { url = "https://files.pythonhosted.org/packages/f2/99/bf1820e6feeabc2f218c24450ec0c995d6a91e8ba0fd3caf042c9e8adb2a/librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c", size = 111832, upload-time = "2026-07-08T12:25:01.148Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" }, + { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" }, + { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" }, + { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" }, + { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" }, + { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" }, + { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, +] + +[[package]] +name = "mypy" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/7e/be536678c6ae49ef058aba4b483d8c7bc104f471479016066f345bc1f5f8/mypy-2.2.0.tar.gz", hash = "sha256:2cdd99d48590dce6f6b7f1961eda75386364398fcdaad86923bc0f0231bf9baf", size = 3950939, upload-time = "2026-07-08T01:37:27.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2e/1ea8028583fef6561d146b51d738d91268201313ecf69e603e808a740e74/mypy-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea6fe1a30f3e7dc574d641497ffead2428046b0f8bc5804d13b4e4392fdc317b", size = 14739569, upload-time = "2026-07-08T01:35:33.202Z" }, + { url = "https://files.pythonhosted.org/packages/ba/67/c0607da57d78358b3b4fbfa90ee8ca5c6bd1dbb997c9648904ad4d8861d8/mypy-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6965829a6d847a0925f51149472bbeb7a39332fb4801972fdfd9cedd9f8d43a4", size = 13821442, upload-time = "2026-07-08T01:33:45.991Z" }, + { url = "https://files.pythonhosted.org/packages/bb/56/0407007d4ec7a762bac20724af1f0a57a9d860186c39e73105b6b8396fa2/mypy-2.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a906bfc9c4c5de3ece6dc4726f8076d56ce9be1720baf0c6f84e926c10262a4", size = 14049813, upload-time = "2026-07-08T01:35:44.282Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d1/d718e006c8fed4bece7a1bebeea6dcd05f31433af552fc45a82fef426379/mypy-2.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91df92625cb3452758f27f4965b000fb05ad89b00c282cc3430a7bd6b0e5389e", size = 14978473, upload-time = "2026-07-08T01:36:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/82/03/dae7299c45a84efb1590875f92652a5beb2da98a2cff9a567995e2583fe4/mypy-2.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d42893d15894fd34f395090e2d78315ba7c637d5ee221683e02893f578c2f548", size = 15224649, upload-time = "2026-07-08T01:35:15.988Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8e/738e4e73d030f20852a81383c74319ca4a9a4fdaadc3a75823588fe2c833/mypy-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3abaeec02cdc72e30a147d99eb81852b6b745a2c8d19607e25241d79b1abbce", size = 11049851, upload-time = "2026-07-08T01:34:39.31Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/6c58caedb3fa5831a9b179687cd7dc252c66a61dca4800e5ba19c8eff82b/mypy-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c39bdb7ceab252d15011e26d3a254b4aaa3bbf121b551febfa301df9b0c69abe", size = 10060307, upload-time = "2026-07-08T01:33:21.852Z" }, + { url = "https://files.pythonhosted.org/packages/d1/be/fbaba7b0ee89874fb11668416dec9e5585c190b676b0796cff26a9290fe8/mypy-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:484a2be712b245ac6e89847141f1f50c612b0a924aa25917e63e6cfcf4da07cd", size = 14928025, upload-time = "2026-07-08T01:37:24.376Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8f/f79a7c5a76671b0f563d4beaa7d99fe90df4500d2c1d2ba1be0432121bcf/mypy-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb0a020dc68480d40e484675558ed637140df1ccbf896a81ba68bca85f2b50a0", size = 13793027, upload-time = "2026-07-08T01:34:33.937Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b9/3db0086bab611d34e26061b86189e6f71de6d22a9b81699a93b006eabcf6/mypy-2.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc361340732ce7108fa0308812caf02bb6868f16112f1efd35bcad88badf3327", size = 14108322, upload-time = "2026-07-08T01:36:08.316Z" }, + { url = "https://files.pythonhosted.org/packages/58/29/4f1e13979a848de2a0fd385462354b58358b6e8b3d9661663e308f6e3d5d/mypy-2.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0179a3a0b833f724a65f22613607cf7ea941ab17ec34fa283f8d6dfe21d9fa9", size = 15190198, upload-time = "2026-07-08T01:36:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/14/f7/7759f6294d9d25d86671957d0974a215a2a24d429526e26a2f603de951c5/mypy-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e0899b13da1e4ba44b880550f247402ce90ffecc71c54b220bcbe7ecb34f394", size = 15424222, upload-time = "2026-07-08T01:33:39.398Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1b/05b212bef4d2234b5f0b551ea53ce0680d8075b2e79861c765f70b590945/mypy-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:511320b17467402e2906130e185abffffa3d7648aff1444fc2abb61f4c8a087d", size = 11135191, upload-time = "2026-07-08T01:35:03.019Z" }, + { url = "https://files.pythonhosted.org/packages/92/51/495e7122f6589948b36d3820a046461906756a0eb1b6dedc13ebfec7815e/mypy-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:7589f370b33dcdd95708f5340f13a67c2c49140957f934b42ef63064d343cca0", size = 10132502, upload-time = "2026-07-08T01:34:04.508Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5f/2d7a9ac5646274cd6e77ce3abcc2a9ece760c2b21f4c4b9f301711e07855/mypy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6968f27347ef539c443ddfd6897e79db525ddb8c856aa8fbf14c34f310ca5193", size = 14931618, upload-time = "2026-07-08T01:33:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8a/1adaa7caaa104f87021b1ac71252d62e646e9b623d77900ac7a0ae252bf3/mypy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3df226d2a0ae2c3b03845af217800a68e2965d4b14914c99b78d3a2c8ae23299", size = 13857718, upload-time = "2026-07-08T01:35:27.555Z" }, + { url = "https://files.pythonhosted.org/packages/1d/15/b11586b5aebbb82213e297fc30a6fcf3bed6a9deea3739cd8dd87621f3fd/mypy-2.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc53553996aca2094216ad9306a6f06c5265d206c1bcb54dd367560bd3557825", size = 14059704, upload-time = "2026-07-08T01:33:57.363Z" }, + { url = "https://files.pythonhosted.org/packages/03/db/071e05ab442596bdf7a845e830d5ef7128a0175281038245b171a6b16873/mypy-2.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:996abf2f0bebf572556c60720e8dc0cf5292b64060fa68d7f2bc9caa48e01b6f", size = 15128719, upload-time = "2026-07-08T01:33:33.855Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1c/c4b84eafb85ee315da72471523cc1bf7d7c42164085c42333601da7a8817/mypy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ec287c2381898c652bf8ff79448627fe1a9ee76d22005181fac7315a485c7108", size = 15378692, upload-time = "2026-07-08T01:34:10.468Z" }, + { url = "https://files.pythonhosted.org/packages/68/a4/59a0ee94877fdfe2958cec9b6add72a75393063c79cb60ab4026dd5e10c2/mypy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2c967a7685fa93fcf1a778b3ebe76e756b28ba14655f539d7b61ff3da69352a", size = 11150911, upload-time = "2026-07-08T01:35:21.603Z" }, + { url = "https://files.pythonhosted.org/packages/90/e4/6a9144be50180ed43d8c92de9b03dff504daa92b5bcc0353e8960799a23b/mypy-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:d59a4b80351ec92e5f7415fcdd008bd77fcbefc7adf9cbc7ffe4eb9f71617734", size = 10125389, upload-time = "2026-07-08T01:36:36.164Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/0aa8d8d197023ca6040f7b25a486cb47037b6350b0d3bae657c8f85fb43f/mypy-2.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1f6c3d76853071409ac58fc0aadfb276a22af5f190fdaa02152a858088a39ebd", size = 14926083, upload-time = "2026-07-08T01:36:02.365Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7c/35bbe0cb10e6699f90e988e537aaf4282a6c16e37f58848a242eb0a98bde/mypy-2.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bdaa177e80cc3292824d4ef3670b5b58771ee8d57c290e0c9c89e7968212332c", size = 13879985, upload-time = "2026-07-08T01:36:31.093Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0c/1597fbebd873e9b63452317740ae3dd32692cec5da180cc65acd96cd28cf/mypy-2.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80923e6d6e7878291f537ee11052f974954c20cb569798429a5dc265eb780b47", size = 14076883, upload-time = "2026-07-08T01:34:21.789Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/2ec021a83ec01b5d522639f78d8b36adade7fa4821db0f48fd6d82e861f3/mypy-2.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f24bd465a09077c8d64be8f19a6646db467a55490fd315fe7871afe6bb9645", size = 15103567, upload-time = "2026-07-08T01:36:47.717Z" }, + { url = "https://files.pythonhosted.org/packages/53/3a/8cb3529f6d6800c7d069935e5c83a05d80263847b8a947cf6b0b16a9e958/mypy-2.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:db34595464869f474708e769413d1d739fc33a69850f253757b9a4cc20bc1fec", size = 15354641, upload-time = "2026-07-08T01:36:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/320bc9a9553f8a9db5e847ec5ded762ef7ed7403c76c4ba2e8181c80e2f0/mypy-2.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b48092132c7b0ef4322773fecae62fc5b0bc339be348badeec8af502122a4a51", size = 7694355, upload-time = "2026-07-08T01:37:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/90/05/bf3b349e2f885cd3aab488111bb9049439c28bc028dac5073350d3df8fbe/mypy-2.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:6fc0e98b95e31755ca06d89f75fafa7820fbb3ea2caace6d83cba17625cd0acb", size = 11329146, upload-time = "2026-07-08T01:35:38.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/a5/558b06e6cfe17ab88bb38f7b370b6bc68a74ba177c9e138db9748e422d2d/mypy-2.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:bc73a5b4d40e8a3e6b12ef82eb0c90430964e34016a36c2aff4e3bfe37ba41f0", size = 10316586, upload-time = "2026-07-08T01:36:25.458Z" }, + { url = "https://files.pythonhosted.org/packages/0b/21/f0b96f19a9b8ba111a45ffbe9508e818b7f6990469b38f6888943f7bfd3a/mypy-2.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6257bd4b4c0ae2148548b869a1ff3758e38645b92c8fe65eca401866c3c551c1", size = 15922565, upload-time = "2026-07-08T01:35:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/18/f2/1dbcb20b0865d5e992541450a8c73f2fcc90f8bd7d8a4b81313e16934870/mypy-2.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dfa22b3ae862ac1ce76f5976ddd402651b5f090bcfd49c6d0484b8983a29eaf8", size = 14816515, upload-time = "2026-07-08T01:34:58.167Z" }, + { url = "https://files.pythonhosted.org/packages/84/a2/18cce9c7d5b4d14010d1f13836da11b234dda917b17ca8671fc32c136997/mypy-2.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30a430bf26fe8cf372f3933fbd83e633d6561868803645a20e4e6d4523f52a3b", size = 15272246, upload-time = "2026-07-08T01:37:08.72Z" }, + { url = "https://files.pythonhosted.org/packages/ce/71/24d720c7924829bd675cbde2d0fa779f50abf676ca617f53d6a8bfef5fa7/mypy-2.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d58655a60e823b1a4c9ebcda072897fb0193c2f3e6f8e7c433e152aa4cb00233", size = 16226295, upload-time = "2026-07-08T01:34:51.861Z" }, + { url = "https://files.pythonhosted.org/packages/b7/5e/785730990fc863ad8340b4ab44ac4ca23270aecff92c180ccdf27f9f5869/mypy-2.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d4452c955caf14e28bb046cbd0c3671272e6381630a8b81b0da9713558148890", size = 16493275, upload-time = "2026-07-08T01:35:09.337Z" }, + { url = "https://files.pythonhosted.org/packages/93/33/55b1edf16f639f153972380d6977b81f65509c5b8f9c86b58b94b7990b03/mypy-2.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:68f5b7f7f755200f68c7181e3dfb28be9858162257690e539759c9f57721e388", size = 11749038, upload-time = "2026-07-08T01:35:50.071Z" }, + { url = "https://files.pythonhosted.org/packages/61/36/67424748a4e65e97f0e05bf00df379dfb6c2d817f82cc3a4ce5c96d99beb/mypy-2.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:78d201accfafce3801d978f2b8dbbd473a9ce364cc0a0dfd9192fe47d977e129", size = 10704254, upload-time = "2026-07-08T01:37:17.971Z" }, + { url = "https://files.pythonhosted.org/packages/28/cb/142c2097ca02c0d295b00625ff946808bdda65acda17d163c680d8a6a474/mypy-2.2.0-py3-none-any.whl", hash = "sha256:ecc138da861e932d1344214da4bae866b21900a9c2778824b51fe2fb47f5180e", size = 2726094, upload-time = "2026-07-08T01:34:00.075Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" }, + { url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" }, + { url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" }, + { url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" }, + { url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/69/a67c69e5f28fc9c99d6f7bd60bd50e91f2fed2423e3b30fb228fa00e51f3/sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba", size = 2161838, upload-time = "2026-06-15T16:05:17.144Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/c8c22b8438bddc0a030157c6ec0f6ef97b3c38effa444bdab2a27af04090/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604", size = 3319402, upload-time = "2026-06-15T16:10:40.002Z" }, + { url = "https://files.pythonhosted.org/packages/90/54/44012d32fd77d991256d2ff793ba3807c51d40cb27a85b4796224f6744df/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd", size = 3319675, upload-time = "2026-06-15T16:12:25.658Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/de0592acaf5906cd7430874392d6f7e8b4a7c8437610953ee2d1501c0b44/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260", size = 3270777, upload-time = "2026-06-15T16:10:42.125Z" }, + { url = "https://files.pythonhosted.org/packages/cb/14/a44c90739c780b362238e4ac3cb19dd0ca40d13e6ddc5daa112166ddab4f/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265", size = 3293940, upload-time = "2026-06-15T16:12:27.156Z" }, + { url = "https://files.pythonhosted.org/packages/65/eb/fbd0f206a330e66f8c602a99c37c4e731f107faed62954b41b01f16dd9d9/sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86", size = 2121183, upload-time = "2026-06-15T16:13:29.905Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fd/005bf80f3cf6e5c62b5dd68616280f51cd012c60840fa74781b3ed7b1623/sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc", size = 2145796, upload-time = "2026-06-15T16:13:31.283Z" }, + { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, + { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, + { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, + { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260518" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, +]