diff --git a/CHANGELOG.md b/CHANGELOG.md index 859215e..722899a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,41 @@ All notable changes to TruthLease are documented here. The project follows [Semantic Versioning](https://semver.org/) once its public contract stabilizes. +## 0.3.0-alpha.1 - 2026-07-22 + +Stale-safe durable resume vertical slice: + +- optional LangGraph `1.2.x` integration with persistent approval, authoritative + revalidation, fenced execution, and explicit ambiguous-outcome recovery; +- digest-minimized checkpoint state containing references, bindings, durable + IDs, phases, and stable decision codes instead of raw effects, target tokens, + evidence payloads, or exception messages; +- SQLite schema v3 `lease_requests` binding for restart-safe, idempotent lease + acquisition across workflow-node replay; +- exact lease-intent conflict detection across artifact, effect, target, + operation, assurance, microsecond TTL, boot epoch, and target precondition; +- recovery of the original lease after consumption or invalidation so an + existing target attempt is resolved rather than replaced; +- public read-only lease and lease-to-attempt accessors for framework adapters; +- real `SqliteSaver` process-restart tests covering policy drift, target-token + drift, duplicate dispatch, response loss after commit, incomplete dependency + capture, reference drift, strict approval types, and raw-value redaction; +- explicit recovery approval bound to the current durable attempt status and + result digest, including pre-checkpoint crash tests; +- conservative same-attempt recovery when local persistence fails after a + possible target commit, while an already persisted receipt returns without a + redundant state read; +- durable terminal truth taking precedence over stale recovery responses and + payload resolvers after an attempt is resolved by another worker; +- migration tests for populated v1 and v2 stores with no synthetic request + backfill; +- architecture decision record, integration guide, updated threat model, and + current competitive boundary. + +The alpha does not turn TruthLease into a workflow engine, authorize arbitrary +LangGraph state, encrypt checkpoints, discover undeclared dependencies, or make +external effects exactly once without a cooperating deduplicating target. + ## 0.2.0-alpha.1 - 2026-07-20 Target-fencing vertical slice: diff --git a/README.md b/README.md index c935ec2..3cdc21d 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ effect.** - **Why it matters:** durable execution can resume an interrupted workflow, but resuming reliably is different from checking that the saved decision is still valid. +- **What the LangGraph integration adds:** a paused effect can survive a process + restart, re-read its authoritative dependencies, and either reuse one durable + lease and target attempt or stop with a stable reconciliation code. The + checkpoint stores references and digests, not the raw effect or target token. - **Concrete example:** a release plan uses policy `v12` and test report `r7`. Policy `v13` arrives while the workflow is paused. TruthLease blocks the old plan, preserves test report `r7`, and identifies only the work that now needs @@ -45,6 +49,7 @@ about what is reusable, stale, or still uncertain. | Short-lived, single-use leases | A stale or substituted effect is rejected at the local gate. | | Bound target preconditions and stable attempt IDs | A cooperating endpoint can reject stale state and deduplicate an ambiguous retry. | | Post-commit fenced receipts | Operators can distinguish a local permit from a target-confirmed effect. | +| Optional LangGraph stale-safe resume | A persisted approval resumes only after authoritative revalidation; crash recovery reuses the same lease and target attempt. | | Explicit assurance and completeness | Operators can see what the runtime actually knows instead of trusting a vague safety claim. | | Lifecycle events and SQLite integrity checks | Accepted lifecycle changes are inspectable and the local store fails closed. Denials are returned decisions, not guaranteed journal entries. | @@ -57,7 +62,7 @@ about what is reusable, stale, or still uncertain. ## Quick start TruthLease requires Python 3.11 or newer and has no third-party runtime -dependencies. +dependencies in its core. The LangGraph adapter is an optional extra. ```bash git clone https://github.com/aantenore/truthlease.git @@ -66,6 +71,12 @@ uv sync --extra dev uv run truthlease demo --reset ``` +For persistent LangGraph approval and resume: + +```bash +uv sync --extra langgraph +``` + The demo constructs this graph: ```mermaid @@ -85,6 +96,75 @@ It then proves that: - an identical change event is idempotent; - a new plan revision can acquire and consume a new lease. +## LangGraph: resume the workflow, re-check the decision + +LangGraph checkpoints preserve workflow position; TruthLease separately checks +whether the saved action is still eligible at the durable-effect boundary. The +integration composes the two without making LangGraph a core dependency: + +```mermaid +flowchart LR + Pause["Approval interrupt"] --> Checkpoint["LangGraph checkpoint"] + Checkpoint --> Resume["Process restart + resume"] + Resume --> Revalidate["Authoritative re-read"] + Revalidate -->|changed| Stop["Reconcile; no target effect"] + Revalidate -->|still current| Lease["Idempotent fenced lease"] + Lease --> Target["One durable target attempt"] + Target -->|response lost| Recover["Recovery interrupt"] + Recover --> Target + Target --> Receipt["Fenced receipt"] +``` + +The public surface is deliberately callback-driven: source revalidation, effect +resolution, target-precondition resolution, evidence production, target adapter, +and checkpointer are all replaceable. + +```python +import sqlite3 + +from langgraph.checkpoint.sqlite import SqliteSaver +from langgraph.types import Command +from truthlease.integrations.langgraph import ( + build_stale_safe_graph, + make_resume_state, + workflow_idempotency_key, +) + +# `runtime`, `plan`, `effect`, `precondition`, `endpoint`, `adapter`, and the +# callback functions below are application-owned objects. +workflow_key = workflow_idempotency_key({"workflow": "release-2026.07"}) +state = make_resume_state( + idempotency_key=workflow_key, + artifact=plan.ref, + effect_ref="effects/release-2026.07", + effect=effect, + precondition_ref="preconditions/deployment/revision-7", + target_precondition=precondition, + target=endpoint, + operation="promote", +) +checkpointer = SqliteSaver(sqlite3.connect("workflow.db", check_same_thread=False)) +graph = build_stale_safe_graph( + runtime, + checkpointer=checkpointer, + revalidate=revalidate_sources, + resolve_effect=resolve_effect, + resolve_precondition=resolve_precondition, + evidence_provider=provide_evidence, + target_adapter=adapter, +) +config = {"configurable": {"thread_id": workflow_key}} +paused = graph.invoke(state, config=config) +resumed = graph.invoke(Command(resume=True), config=config) +``` + +The snippet shows the composition contract; the callbacks and fenced target must +be supplied by the host application. Use immutable, versioned references and +return the exact original effect and precondition during ambiguous-attempt +recovery. Node names and checkpoint fields are versioned persistence contracts. +See the complete [LangGraph integration guide](docs/langgraph-integration.md) and +[ADR 0003](docs/adr/0003-langgraph-stale-safe-resume.md). + ## Python API ```python @@ -297,6 +377,7 @@ the invalidation closure. Read the [architecture](https://github.com/aantenore/truthlease/blob/main/docs/architecture.md), +[LangGraph stale-safe resume guide](https://github.com/aantenore/truthlease/blob/main/docs/langgraph-integration.md), [target-fencing contract](https://github.com/aantenore/truthlease/blob/main/docs/spec/coherence-v2.md), [base coherence contract](https://github.com/aantenore/truthlease/blob/main/docs/spec/coherence-v1.md), [threat model](https://github.com/aantenore/truthlease/blob/main/docs/threat-model.md), and @@ -322,10 +403,12 @@ These public projects are complementary rather than one growing framework: - distributed transactions, universal rollback, or exactly-once external effects; - an authentication, authorization, or policy engine. -Use Restate, Temporal, or LangGraph for durable execution; Drasi or Debezium for -high-volume change capture; a policy engine for authorization; and target-side -idempotency plus fencing for durable effects. TruthLease connects their version -evidence to the artifacts that an agent may later use. +Use Restate, Temporal, or LangGraph for general durable execution; Drasi or +Debezium for high-volume change capture; a policy engine for authorization; and +target-side idempotency plus fencing for durable effects. TruthLease connects +their version evidence to the artifacts that an agent may later use. Its +LangGraph adapter is a narrow executable composition for one approval-to-effect +boundary, not a replacement workflow engine. ## Why a separate project @@ -362,7 +445,7 @@ uv run ruff check . uv run mypy uv run pytest --cov=truthlease --cov-report=term-missing uv run python benchmarks/benchmark_fence_correctness.py -uv build +uv build --no-sources ``` This is an executable alpha. See diff --git a/docs/adr/0003-langgraph-stale-safe-resume.md b/docs/adr/0003-langgraph-stale-safe-resume.md new file mode 100644 index 0000000..68154fa --- /dev/null +++ b/docs/adr/0003-langgraph-stale-safe-resume.md @@ -0,0 +1,147 @@ +# ADR 0003: LangGraph stale-safe resume + +- Status: accepted +- Date: 2026-07-22 +- Release: `0.3.0-alpha.1` + +## Context + +LangGraph can checkpoint an interrupted workflow and restart its node later. +That durability does not imply that the policy, record, schema, approval, or +target token behind a saved action is still current. Its interrupt contract also +means the node restarts from the beginning, so side effects around interruption +must be idempotent. + +TruthLease `0.2` could fence a target commit and reuse a prepared attempt, but a +workflow crash immediately after lease acquisition could rerun the node and +acquire a new lease before the lease ID itself reached the workflow checkpoint. +The framework integration therefore needs a durable identity below the graph +checkpoint boundary. + +## Decision + +Add two independent, composable pieces: + +1. SQLite schema v3 stores a digest-only `lease_requests` binding from one + application idempotency key to one lease-intent digest and one lease ID. +2. An optional LangGraph graph composes approval, authoritative revalidation, + fenced lease acquisition, target execution, and same-attempt recovery. + +The core remains free of mandatory third-party runtime dependencies. LangGraph +and its SQLite saver are installed through the `langgraph` extra. + +## Lease acquisition identity + +`TruthLease.acquire_lease()` accepts an optional algorithm-prefixed +`idempotency_key`. The stored intent digest binds the artifact reference, effect, +target, operation, assurance, TTL at microsecond precision, boot epoch, and +target-precondition descriptor. + +- First use performs the normal freshness checks and atomically stores the lease + and request binding. +- Identical reuse returns the exact original lease, including terminal state. +- Reuse for different intent fails closed with `ConflictError`. +- No raw effect, target token, or workflow label is stored in the binding. + +Returning a consumed lease is deliberate: its durable attempt is the only safe +identity for resolving an ambiguous target outcome. + +## Graph structure + +The graph has four versioned nodes: + +```text +START -> approval_v1 -> authorize_v1 -> execute_fenced_v1 + -> recover_indeterminate_v1 + -> execute_fenced_v1 +``` + +- `approval_v1` has no side effect before `interrupt()`. +- `authorize_v1` re-reads authoritative sources, verifies resolved digests, and + acquires the idempotent fenced lease. It performs no target effect. +- `execute_fenced_v1` resolves again, prepares one durable attempt, and invokes + the target outside the database transaction. +- `recover_indeterminate_v1` requires an explicit boolean before retrying that + same attempt. + +Node names and state fields are a persistence contract. Incompatible evolution +requires versioned names and a checkpoint migration strategy. + +## Checkpoint minimization + +The graph state contains only references, digests, IDs, phase, approval, and +stable decision codes. Recovery approval is bound to a digest of the current +attempt status and result. Raw effects, opaque target tokens, evidence payloads, +and exception messages stay behind replaceable callbacks. + +This is data minimization, not encryption or tamper resistance. The host and +checkpoint store remain trusted, references must not contain secrets, and +low-entropy identifiers need an application-owned keyed digest if guessing is a +concern. + +## Crash boundaries + +| Crash point | Recovery behavior | +| --- | --- | +| Before approval checkpoint | No lease and no target effect exist. | +| During authoritative read | The node reruns; source adapters must use their normal compare-and-swap discipline. | +| After lease acquisition but before graph checkpoint | The node reacquires by the same idempotency key and receives the original lease. | +| After attempt prepare but before target call | A prepared attempt is conservatively treated as potentially dispatched and requires recovery approval before the same request can be sent. | +| After target commit but before local result record | The prepared attempt is treated as potentially dispatched and requires recovery approval; the target then resolves the same attempt ID and request fingerprint. | +| After local receipt but before graph checkpoint | Re-execution returns the terminal receipt without redispatch. | + +Recovery approval remains valid while resolving one unchanged durable attempt +basis, including across a crash of that approved node. If status or result digest +changes, the basis changes and the old approval is not reused. This authorizes +resolution of one logical attempt, not an unbounded sequence of new effect +identities; target-side deduplication remains required for repeated transport. +If the attempt becomes terminal while the recovery interrupt is paused, that +durable outcome takes precedence over the stale human response and is surfaced +without another effect/precondition resolution or target dispatch. + +## Alternatives considered + +### Store the full effect in LangGraph state + +Rejected. It couples checkpoint retention to application payload sensitivity and +makes accidental prompt, credential, or target-token persistence likely. + +### Trust the saved checkpoint without authoritative revalidation + +Rejected. Durable position and current authority are different properties. + +### Acquire a fresh lease on every node retry + +Rejected. A crash between target commit and checkpoint could create a second +attempt and duplicate the durable effect. + +### Put LangGraph in the core dependency set + +Rejected. TruthLease is framework-neutral and its deterministic core remains +usable in small services and other workflow engines. + +### Build a workflow engine in TruthLease + +Rejected. Scheduling, general human-task management, compensation, and arbitrary +graph execution remain outside this project's boundary. + +## Consequences + +Positive: + +- a real durable-orchestrator integration now exercises the commit-time contract; +- pause/restart and ambiguous-response recovery preserve one lease and attempt; +- framework checkpoints avoid raw effects and target tokens; +- callbacks and checkpointers remain replaceable. + +Costs and limitations: + +- callers must keep immutable effect and precondition references resolvable for + the full recovery horizon; +- TTL, boot epoch, and every other intent component must remain identical during + recovery; +- target deduplication retention must outlive that recovery horizon; +- SQLite schema version advances from 2 to 3; +- paused state version 1 and node names now require compatibility discipline; +- the initial adapter is synchronous and covers one fenced effect, not a general + multi-target workflow. diff --git a/docs/architecture.md b/docs/architecture.md index 1612cac..ef14f2b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -42,6 +42,7 @@ flowchart TB Read["Authoritative reads"] Replan["Replanner"] Fence["Target fence"] + Durable["Optional durable orchestrator"] end subgraph Core["TruthLease deterministic core"] @@ -60,15 +61,17 @@ flowchart TB Graph --> Lease --> Gate --> Decision["Local decision / receipt"] Decision --> Host["Embedding application"] Host --> Fence + Durable --> Host Host -->|asks for plan| Inv Inv -->|returns plan| Host Host --> Replan Obs & Art & Inv & Lease & Gate --> Events ``` -The deterministic core, SQLite adapter, and strict HTTP conformance profile are -implemented in `0.2.0-alpha.1`. Provider integrations remain explicit extension -boundaries rather than hidden dependencies. +The deterministic core, SQLite adapter, strict HTTP conformance profile, and an +optional LangGraph stale-safe resume composition are implemented in +`0.3.0-alpha.1`. Provider and framework integrations remain explicit extension +boundaries rather than hidden core dependencies. ## Domain objects @@ -136,6 +139,14 @@ artifact therefore cannot relabel declared upstream evidence as observed. Leases are single-use. `consumed`, `expired`, `invalidated`, and `revoked` are terminal states. +An optional digest-only lease request identity sits beside the lease. It binds a +caller-provided idempotency digest to the complete lease intent: artifact, +effect, target, operation, assurance, TTL, boot epoch, and target-precondition +descriptor. An identical retry can therefore recover the original lease after a +workflow-node restart, including when that lease is already consumed. Reusing +the key for a different intent fails closed. The binding never stores the raw +effect, opaque target token, or raw workflow label. + An optional target-precondition binding is stored separately so the closed v1 lease read model remains unchanged. It binds the adapter and mechanism, a digest of the opaque expected token, replay assurance, and a digest of the exact @@ -185,6 +196,47 @@ and verifies its original request fingerprint before evaluating the current ETag, so retry after a lost response cannot turn an earlier commit into `412`. A `2xx` without the required evidence remains indeterminate. +## Optional durable-resume composition + +TruthLease does not become a workflow engine. The LangGraph integration is an +adapter above the same public core and target-fencing protocol: + +```mermaid +sequenceDiagram + participant U as Human / caller + participant G as LangGraph + checkpointer + participant L as TruthLease + participant S as Authoritative sources + participant T as Deduplicating target + G-->>U: approval interrupt with refs and digests + U->>G: resume(true) + G->>S: application callback re-reads sources + S->>L: commit changed observations, if any + G->>L: acquire fenced lease by intent digest + alt artifact still current + L-->>G: original or new lease + G->>L: prepare same durable attempt + G->>T: conditional commit(attempt ID) + T-->>G: committed / not committed / indeterminate + G->>L: record bound result + else stale, incomplete, or conflicting + L-->>G: stable fail-closed error code + end +``` + +LangGraph persists workflow position. TruthLease separately evaluates current +artifact eligibility and owns the durable lease/attempt identities. The graph +stores only state version, references, digests, IDs, phase, approval, and stable +decision codes. Raw effects and preconditions are reconstructed through host +callbacks and verified twice: after authoritative revalidation and immediately +before execution. + +Ambiguous recovery deliberately skips new authorization and resolves the +already prepared target attempt. The original immutable references must remain +available; if they cannot be reconstructed exactly, recovery stops. See +[the integration guide](langgraph-integration.md) and +[ADR 0003](adr/0003-langgraph-stale-safe-resume.md). + ## Freshness predicate For artifact `a`, store state `S`, and time `t`: @@ -270,7 +322,7 @@ enabled. ## Dependency direction ```text -CLI / target adapter +CLI / optional orchestrator integration / target adapter ↓ application service (TruthLease) ↓ diff --git a/docs/competitive-landscape.md b/docs/competitive-landscape.md index 13b964d..e4f8e1c 100644 --- a/docs/competitive-landscape.md +++ b/docs/competitive-landscape.md @@ -1,6 +1,6 @@ # Competitive landscape -Reviewed on 2026-07-20 using primary project documentation, standards, and +Reviewed on 2026-07-22 using primary project documentation, standards, and original papers. ## Finding @@ -11,7 +11,7 @@ transaction runtime. | Existing system | What it already solves | TruthLease boundary | | --- | --- | --- | -| [Restate](https://docs.restate.dev/ai/patterns/durable-agents), [Temporal](https://docs.temporal.io/activity-definition#idempotency), [LangGraph](https://docs.langchain.com/oss/python/langgraph/persistence) | Durable steps, checkpoints, recovery, and application-level idempotency patterns. | Validate whether the external premises of a saved artifact still match, then bind that lineage to a target commit; the target still owns effect idempotency. | +| [Restate](https://docs.restate.dev/ai/patterns/durable-agents), [Temporal](https://docs.temporal.io/activity-definition#idempotency), [LangGraph](https://docs.langchain.com/oss/python/langgraph/persistence) | Durable steps, checkpoints, recovery, and application-level idempotency patterns. | Validate whether the external premises of a saved artifact still match, then bind that lineage to a target commit; the target still owns effect idempotency. The optional LangGraph adapter makes this composition executable without absorbing the workflow engine. | | [HTTP `If-Match`](https://www.rfc-editor.org/rfc/rfc9110.html#section-13.1.1), [Google AIP-154](https://google.aip.dev/154), [Kubernetes `resourceVersion`](https://kubernetes.io/docs/reference/kubernetes-api/definitions/object-meta-v1-meta/) | Established optimistic-concurrency tokens and conditional writes. | Bind a target token to the artifact dependency closure, exact effect, stable attempt, coverage, and post-commit receipt rather than inventing a new token. | | [`Idempotency-Key` Internet-Draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/) | Proposed request-key, fingerprint, replay, and conflict semantics; it is not a final RFC. | Use those semantics as an explicit adapter contract and keep ambiguous outcomes distinct from confirmed commits. | | [Drasi](https://drasi.io/concepts/) | Change detection, continuous queries, and reactions across data sources. | Map a source change onto dependent agent artifacts, leases, and reconciliation scope. | @@ -37,14 +37,18 @@ Build here: - local use-time gate and receipts for accepted consumption; - immutable target attempts that bind closure, effect, precondition, protected scope, and replay contract; -- core-derived post-commit receipts with explicit coverage and ambiguity. +- core-derived post-commit receipts with explicit coverage and ambiguity; +- digest-only lease-request identity for restart-safe recovery; +- one narrow LangGraph approval-to-effect composition with minimized checkpoint + state and same-attempt ambiguous recovery. Integrate later: - CloudEvents for transport; - W3C PROV for lineage export; - MCP, Drasi, or Debezium for source change hints; -- Restate or Temporal for durable reconciliation work; +- Restate or Temporal for durable reconciliation work, or other LangGraph graphs + beyond the supplied single-effect composition; - provider-specific conditional HTTP or datastore compare-and-set adapters; - [AgenticStrata](https://github.com/aantenore/AgenticStrata) for wider execution contracts and evidence; diff --git a/docs/delivery-contract.md b/docs/delivery-contract.md index dcb59b1..5533ca3 100644 --- a/docs/delivery-contract.md +++ b/docs/delivery-contract.md @@ -1,4 +1,4 @@ -# Delivery contract: TruthLease 0.2 alpha +# Delivery contract: TruthLease 0.3 alpha ## Objective @@ -6,6 +6,9 @@ Deliver an executable, provider-neutral coherence control plane that prevents an application from dispatching an effect through a lease after a known dependency of its source artifact changed, and can produce a post-commit receipt when one cooperating target atomically enforces the bound state. +The optional LangGraph composition must also demonstrate that a persisted +approval resumes through a fresh coherence check and retains one target-attempt +identity across process restart. ## Must @@ -36,11 +39,18 @@ cooperating target atomically enforces the bound state. distinct and fail closed. - Never redispatch an attempt already known as committed or not committed, and never let a late transport ambiguity erase a terminal fact. +- Bind an optional digest-only workflow idempotency key to the complete lease + intent and recover the exact original lease after workflow-node replay. +- Persist only minimized references, digests, IDs, phases, booleans, and stable + codes in the supplied LangGraph state contract; keep raw effects, target + tokens, evidence payloads, and exception messages behind host callbacks. +- Revalidate authoritative dependencies after approval and verify effect and + precondition references again immediately before target dispatch. - Make guarantee and dependency-completeness limits visible. - Cap derived and leased completeness at the weakest required transitive dependency. -- Run with no model or hosted service and no third-party Python runtime - dependency. +- Run the deterministic core with no model, hosted service, or third-party + Python runtime dependency; keep LangGraph behind an optional extra. ## Should @@ -49,7 +59,7 @@ cooperating target atomically enforces the bound state. - Emit an append-only, deduplicated history of accepted lifecycle events. - Ship architecture, protocol, threat-model, and benchmark documentation. - Preserve a byte-identical JSON v1 contract and ship a self-contained v2 - contract plus transactional SQLite v1-to-v2 migration. + contract plus transactional SQLite v1/v2-to-v3 migration. ## Out of scope @@ -80,6 +90,11 @@ cooperating target atomically enforces the bound state. same target-side critical section and one effect under ambiguous retry. - A target commit test proves no SQLite write transaction remains open during adapter execution. +- Real LangGraph SQLite checkpoints survive close/reopen and cover policy drift, + target-token drift, duplicate workflow dispatch, response loss after target + commit, incomplete dependency capture, and reference drift. +- Raw effect and target-token fixtures are absent from both persisted SQLite + files after the resume path. - The published JSON Schema is identified as an alpha read-model subset, not the complete behavioral contract. - SQLite uses rollback journaling, full synchronization, foreign keys, and a diff --git a/docs/langgraph-integration.md b/docs/langgraph-integration.md new file mode 100644 index 0000000..b20e048 --- /dev/null +++ b/docs/langgraph-integration.md @@ -0,0 +1,254 @@ +# LangGraph stale-safe resume + +## What it solves + +A durable workflow can remember exactly where it paused and still resume an old +decision after the facts behind that decision changed. This integration makes +the resume boundary explicit: + +1. LangGraph persists the workflow position and waits for approval. +2. On resume, the host re-reads authoritative sources into TruthLease. +3. TruthLease issues or recovers one intent-bound fenced lease. +4. The raw effect and target precondition are resolved again and compared with + their checkpointed digests. +5. A cooperating target commits once, rejects the stale precondition, or leaves + the attempt explicitly indeterminate. +6. Ambiguous recovery reuses the same lease and attempt; it never creates a new + effect identity. + +LangGraph documents that interrupted nodes restart from the beginning and that +side effects before an interrupt must be idempotent. It also describes +checkpoints as persisted workflow state, not as proof that external authority is +still current. TruthLease supplies that separate commit-time coherence boundary. + +- [LangGraph interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts) +- [LangGraph persistence](https://docs.langchain.com/oss/python/langgraph/persistence) +- [LangGraph checkpointer integrations](https://docs.langchain.com/oss/python/integrations/checkpointers/index) +- [Commit-time authorization paper](https://arxiv.org/abs/2607.10487) + +## Install + +The core keeps zero mandatory third-party runtime dependencies: + +```bash +pip install truthlease +``` + +Install the optional integration for LangGraph `1.2.x` and the SQLite +checkpointer `3.1.x`: + +```bash +pip install "truthlease[langgraph]" +``` + +SQLite is appropriate for local and small synchronous workflows. The graph +accepts the LangGraph `BaseCheckpointSaver` interface, so a host can supply a +different durable saver. Backend setup, concurrency, encryption, retention, and +backup remain host responsibilities. + +## Replaceable boundaries + +`build_stale_safe_graph()` accepts six application-owned boundaries: + +| Boundary | Contract | +| --- | --- | +| `checkpointer` | Persist and restore LangGraph checkpoints. | +| `revalidate(runtime, artifact_ref)` | Re-read authoritative sources and commit any changed observations before returning. | +| `resolve_effect(effect_ref)` | Return the exact raw effect represented by the immutable reference. | +| `resolve_precondition(precondition_ref)` | Return the exact original target precondition, including the opaque token. | +| `evidence_provider(runtime, lease)` | Produce fresh, allowlisted, lease-bound `AssuranceEvidence` for the first target attempt. | +| `target_adapter` | Enforce the target precondition and deduplicate by the stable attempt identity. | + +No callback is hardcoded to HTTP, a model provider, a policy system, or one +checkpoint backend. + +## Checkpoint contract + +The graph persists only these classes of value: + +- schema and phase; +- digest-only idempotency key; +- artifact ID and revision; +- effect and precondition references plus their canonical digests; +- target and operation; +- lease, attempt, and receipt IDs; +- approval booleans, a digest of the exact recovery basis, and stable + decision/reconciliation codes. + +It does not place these values in graph state: + +- prompt or conversation text; +- raw effect body; +- raw target precondition token; +- revalidation evidence payload; +- callback exception message. + +References are not automatically secret. Use opaque, immutable, versioned +references and do not put credentials or payloads into them. The supplied +`workflow_idempotency_key()` is an unkeyed canonical digest that removes a raw +workflow label from the two stores; it is not encryption and does not hide a +low-entropy value from guessing. Use an application-owned keyed digest when +that distinction matters. + +Use the same digest as the LangGraph `thread_id` to avoid putting the raw +workflow identifier into checkpointer metadata: + +```python +workflow_key = workflow_idempotency_key({"workflow": "release-2026.07"}) +config = {"configurable": {"thread_id": workflow_key}} +``` + +The following node names and state fields are a persistence contract for paused +workflows: + +```text +approval_v1 +authorize_v1 +execute_fenced_v1 +recover_indeterminate_v1 +``` + +Do not rename them or change their meaning while checkpoints created with state +version `1` may still resume. A future incompatible graph must add an explicit +state migration or use new versioned node names. + +## Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> awaiting_approval + awaiting_approval --> reconcile: denied or invalid answer + awaiting_approval --> approved: exact boolean true + approved --> reconcile: source, reference, or lease check fails + approved --> ready_to_execute: idempotent fenced lease acquired + ready_to_execute --> committed: verified fenced receipt + ready_to_execute --> reconcile: target certainly did not commit + ready_to_execute --> awaiting_recovery: target outcome indeterminate + awaiting_recovery --> reconcile: decline or invalid answer + awaiting_recovery --> ready_to_execute: exact boolean true +``` + +### Initial approval + +`make_resume_state()` hashes the raw effect and precondition but returns only +their references and digests. The first invocation reaches a LangGraph +`interrupt()`. Approval accepts the exact JSON boolean `true`; strings such as +`"yes"` fail closed. + +### Authorize after resume + +The authorization node first runs `revalidate`. It then resolves the effect and +precondition and compares both with their checkpointed digests. Only then does +it call `acquire_lease(..., idempotency_key=...)` at `fenced` assurance. + +SQLite schema v3 binds that idempotency key to a digest of: + +- artifact ID and revision; +- effect digest; +- target and operation; +- requested assurance; +- exact lease TTL in microseconds; +- runtime boot epoch; +- target-precondition descriptor digest. + +An identical retry returns the original lease even when it is already consumed +or invalidated, which is necessary to recover its existing target attempt. A +different intent with the same key raises `CONFLICT` before target dispatch. + +### Execute + +The execution node resolves and verifies the effect and precondition again. For +a new attempt it asks the host for fresh evidence, durably prepares and consumes +the lease, then calls the target outside the TruthLease SQLite transaction. + +For an existing attempt, it does not mint new evidence or a new attempt. A +terminal committed result returns the existing fenced receipt without calling +the target again. A certain target rejection ends in reconciliation. + +### Recover an ambiguous target call + +If a response is lost after the target may have committed, TruthLease records +the attempt as `indeterminate` and the graph pauses again. An approved recovery +resolves the original immutable references and dispatches the same prepared +attempt. A conforming target must look up a known attempt and verify its original +request fingerprint before considering the current precondition. + +Recovery approval is bound to a canonical digest of the attempt ID, durable +status, and recorded result digest. If another worker changes that durable +outcome while approval is paused, the old approval cannot authorize a dispatch; +the graph re-evaluates the new basis first. A terminal committed, rejected, or +disputed outcome always overrides the stale recovery response, including a +decline or malformed response, and is reported without resolving the raw effect +again. Approval does remain durable through a process crash while resolving the +same unchanged ambiguous basis. That may repeat a transport call, but never +creates a new logical attempt; target deduplication is therefore mandatory. + +This recovery step determines what happened to the earlier attempt; it is not a +new authorization decision. If a still-ambiguous attempt's original effect or +precondition can no longer be resolved exactly, the graph stops rather than +guessing. + +## Outcomes + +| `phase` | Example `decision_code` | Meaning | +| --- | --- | --- | +| `committed` | `FENCED_COMMIT_RECORDED` | TruthLease verified a full-coverage, deduplicated target commit and stored its receipt. | +| `reconcile` | `ARTIFACT_NOT_FRESH` | An authoritative change invalidated the saved artifact before effect dispatch. | +| `reconcile` | `AUTHORITATIVE_REVALIDATION_FAILED` | The authoritative source could not be read; exception details were not checkpointed. | +| `reconcile` | `EFFECT_REFERENCE_UNAVAILABLE` | The saved effect reference could not be resolved. | +| `reconcile` | `EFFECT_REFERENCE_CHANGED` | The effect resolver no longer returned the checkpointed effect. | +| `reconcile` | `PRECONDITION_REFERENCE_UNAVAILABLE` | The saved precondition reference could not be resolved. | +| `reconcile` | `PRECONDITION_REFERENCE_CHANGED` | The original target precondition could not be reconstructed exactly. | +| `reconcile` | `EVIDENCE_PROVIDER_FAILED` | Fresh assurance evidence could not be constructed, before any target dispatch. | +| `reconcile` | `TARGET_PRECONDITION_REJECTED` | The cooperating target certainly did not apply the effect. | +| `awaiting_recovery` | `TARGET_OUTCOME_INDETERMINATE` | The attempt may have committed; only same-attempt recovery is allowed. | +| `reconcile` | `CONFLICT` | The workflow key was reused for a different lease intent. | + +These codes are checkpoint-safe summaries. Persist detailed operational errors +in an application-owned, access-controlled telemetry sink rather than graph +state. + +## Tested failure matrix + +The executable suite uses the real LangGraph `SqliteSaver` and closes/reopens +its database to simulate process restart. It verifies: + +- clean pause, restart, approval, and one fenced commit; +- policy change during the pause with zero target dispatches; +- target ETag change with a certain non-commit; +- duplicate workflow dispatch with one target effect and one evidence read; +- response loss after target commit, same-attempt recovery, and one target + effect; +- process death after a prepared or indeterminate attempt, with zero redispatch + before a recovery decision; +- local result-persistence failure after a target commit, with same-attempt + recovery instead of a false terminal failure; +- an attempt result changing while recovery approval is paused, with the old + recovery basis refused; +- unknown dependency completeness with zero target dispatches; +- missing, invalid, and changed effect or precondition references with zero + target dispatches; +- authoritative-read and evidence-provider failures reduced to redacted, + stage-specific decision codes; +- idempotency-key reuse for different intent failing closed; +- exact-boolean approval and recovery decisions; +- absence of raw effect and raw target token in both checkpoint and TruthLease + database bytes. + +The suite does not prove the behavior of an arbitrary target adapter, source +reader, checkpointer deployment, or compromised host. Those remain explicit +trust boundaries. + +## Non-goals + +This adapter is not: + +- a general LangGraph agent template; +- an IAM or policy engine; +- a compensation or multi-target transaction coordinator; +- a guarantee of exactly-once external effects without target deduplication; +- a replacement for application telemetry, checkpoint encryption, or backups; +- an automatic dependency-discovery system. + +The architectural decision and migration consequences are recorded in +[ADR 0003](adr/0003-langgraph-stale-safe-resume.md). diff --git a/docs/spec/coherence-v2.md b/docs/spec/coherence-v2.md index 851c05a..5233e7a 100644 --- a/docs/spec/coherence-v2.md +++ b/docs/spec/coherence-v2.md @@ -279,3 +279,8 @@ ineligible for the new path. The local `gate_receipts` history remains unchanged. Fenced receipts are stored separately so a post-commit fact never overwrites a pre-dispatch decision. + +TruthLease `0.3` advances the physical SQLite schema to version 3 by adding the +digest-only `lease_requests` recovery binding. That additive table does not +change this v2 target-fencing result contract; its workflow-replay semantics are +defined in [ADR 0003](../adr/0003-langgraph-stale-safe-resume.md). diff --git a/docs/threat-model.md b/docs/threat-model.md index 62114ad..ecdbb51 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -2,7 +2,7 @@ ## Scope and trust assumptions -The `0.2` alpha assumes: +The `0.3` alpha assumes: - the host, operating system, Python process, and SQLite file owner are trusted; - one application or physically separate database owns each trust domain; @@ -11,7 +11,9 @@ The `0.2` alpha assumes: - external sources are authoritative only when registered by the embedding application; - a target that claims conditional commit and deduplication implements both - atomically for the scope declared by its adapter. + atomically for the scope declared by its adapter; +- durable-orchestrator checkpoints, effect/precondition reference stores, and + the callbacks that resolve them are inside the trusted host boundary. TruthLease does not claim secure multi-tenancy inside one SQLite database. @@ -22,6 +24,7 @@ TruthLease does not claim secure multi-tenancy inside one SQLite database. - active lease secrecy and single-use state; - gate receipt and accepted-lifecycle event history integrity; - immutable attempt, precondition, scope, request, and target-result bindings; +- lease-request identity and immutable workflow-reference bindings; - distinction between confirmed, rejected, and indeterminate remote outcomes; - availability of the invalidation and gate path. @@ -37,6 +40,12 @@ TruthLease does not claim secure multi-tenancy inside one SQLite database. | Source changes after local gate | Explicit assurance label and residual-race text. | Only target-side fencing closes this race. | | Target changes after local prepare | A trusted adapter must atomically enforce the bound precondition; the HTTP profile uses strong `If-Match`. | The adapter assertion cannot prove a dishonest or incorrectly implemented target. | | Duplicate effect after a lost response | The attempt ID is stable and the target contract declares deduplicated replay; recovery reuses the same request fingerprint. | If target retention expires before resolution, the outcome remains indeterminate and automatic retry stops. | +| Duplicate workflow-node execution | A digest-only idempotency key atomically binds the full lease intent and recovers the original lease, including its consumed attempt. Ambiguous recovery approval is bound to the attempt status and result digest. | Different workflow keys can intentionally create different leases; an approved resolution may repeat transport after a crash and therefore still requires target deduplication. | +| Stale durable checkpoint | Resume performs an authoritative re-read, verifies immutable effect and precondition references, and acquires a current fenced lease before first dispatch. | An undeclared dependency or dishonest authoritative source remains outside the guarantee. | +| Reference changes between authorization and execution | The execute node resolves and verifies the effect and precondition a second time before target dispatch. | A compromised resolver or host can substitute both data and control flow. | +| Raw payload leakage through checkpoint state | The supplied state contract stores references, digests, IDs, booleans, phases, and stable codes; tests scan both SQLite stores for raw effect and target-token fixtures. | References, LangGraph thread IDs, host logs, and custom callback telemetry can still leak data if the application puts secrets in them. | +| Tampered or guessed checkpoint digest | Core intent binding detects inconsistent reuse, and changed references fail closed. | Digests are not signatures or encryption; the trusted-host assumption remains, and low-entropy identifiers need a keyed digest for confidentiality. | +| Exception text persisted by integration | Callback failures are reduced to stage-specific stable codes before being returned as graph state. | Failures outside the caught node boundary or application logging can still expose exception details. | | False global fencing from a narrow token | The scope digest names the exact protected observation snapshots; only full closure coverage can derive `fenced`. | The embedding application must describe which facts the target token actually protects. | | Forged success response | The result must bind the attempt, request, closure, effect, precondition, scope, adapter, target, and operation. | A compromised trusted adapter or host can still fabricate evidence. | | Conflicting result replay | Indeterminate evidence may resolve under the same attempt. A late transport ambiguity cannot erase a known terminal fact, while different terminal evidence marks the attempt disputed, invalidates its historical receipt, and fails closed. | Operators must investigate the target and adapter before recovery. | @@ -93,6 +102,15 @@ receipt. `observed` and `revalidated` never claim that guarantee. authoritative verification. - Reuse the original attempt and request fingerprint when resolving an ambiguous target call; never mint a fresh key merely because the caller timed out. +- Derive one stable, digest-only workflow key per logical effect and use it for + both TruthLease idempotency and the LangGraph `thread_id`; do not place a raw + user, prompt, credential, or business identifier in either field. +- Keep effect and precondition references immutable and resolvable for at least + the full checkpoint and target-deduplication recovery horizon. +- Treat LangGraph state version and node names as a persistence contract. Migrate + paused checkpoints before renaming either. +- For production checkpointers, configure access control, encryption, retention, + concurrency, and backups independently of TruthLease. - Keep target deduplication retention longer than the maximum retry and recovery horizon. When retention is unknown or expired, stop automatically. - Treat partial scope as partial even when the target commit succeeds. Separate diff --git a/pyproject.toml b/pyproject.toml index 9ba4093..d3d9b0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "truthlease" -version = "0.2.0a1" +version = "0.3.0a1" description = "A deterministic coherence control plane that stops agents from using artifacts after their declared dependencies change." readme = "README.md" requires-python = ">=3.11" @@ -16,8 +16,11 @@ keywords = [ "agents", "ai", "coherence", + "durable-execution", "freshness", + "idempotency", "invalidation", + "langgraph", "lineage", ] classifiers = [ @@ -32,10 +35,16 @@ classifiers = [ dependencies = [] [project.optional-dependencies] +langgraph = [ + "langgraph>=1.2.9,<1.3", + "langgraph-checkpoint-sqlite>=3.1,<3.2", +] dev = [ "check-wheel-contents>=0.6,<0.7", "hypothesis>=6.130", "jsonschema>=4.23", + "langgraph>=1.2.9,<1.3", + "langgraph-checkpoint-sqlite>=3.1,<3.2", "mypy>=1.15", "pytest>=8.3", "pytest-cov>=6.0", diff --git a/src/truthlease/__init__.py b/src/truthlease/__init__.py index 4d27fc4..7033be2 100644 --- a/src/truthlease/__init__.py +++ b/src/truthlease/__init__.py @@ -73,4 +73,4 @@ "load_coherence_schema", ] -__version__ = "0.2.0a1" +__version__ = "0.3.0a1" diff --git a/src/truthlease/engine.py b/src/truthlease/engine.py index aecb771..b91b0b1 100644 --- a/src/truthlease/engine.py +++ b/src/truthlease/engine.py @@ -402,16 +402,51 @@ def acquire_lease( required_assurance: AssuranceLevel = AssuranceLevel.OBSERVED, ttl: timedelta = timedelta(seconds=30), target_precondition: TargetPrecondition | None = None, + idempotency_key: str | None = None, ) -> Lease: - """Bind a short-lived, single-use lease to an exact artifact and effect.""" + """Bind a short-lived, single-use lease to an exact artifact and effect. + + A digest-only ``idempotency_key`` makes acquisition restart-safe. An + identical retry returns the originally issued lease even after it was + consumed; reusing the key for different intent is rejected. + """ _require_text("target", target) _require_text("operation", operation) if ttl <= timedelta(0): raise ValueError("lease ttl must be positive") + if idempotency_key is not None: + _require_digest("idempotency_key", idempotency_key) effect_digest = digest_effect(effect, operation=operation, target=target) + intent_digest = _lease_request_intent_digest( + artifact=artifact, + effect_digest=effect_digest, + target=target, + operation=operation, + required_assurance=required_assurance, + ttl=ttl, + boot_epoch=self.boot_epoch, + target_precondition=target_precondition, + ) with self.store.transaction(write=True) as tx: now = self.clock.now() + if idempotency_key is not None: + existing_request = tx.get_lease_request(idempotency_key) + if existing_request is not None: + existing_intent_digest, existing_lease_id = existing_request + if existing_intent_digest != intent_digest: + raise ConflictError( + "idempotency key is already bound to a different lease intent", + details={ + "idempotency_key": idempotency_key, + "existing_intent_digest": existing_intent_digest, + "requested_intent_digest": intent_digest, + }, + ) + existing_lease = tx.get_lease(existing_lease_id) + if existing_lease is None: + raise RuntimeError("lease request references a missing lease") + return existing_lease self._raise_if_faulted(tx) current = tx.require_artifact(artifact) current = replace( @@ -537,6 +572,13 @@ def acquire_lease( protected_observations=target_precondition.protected_observations, created_at=utc_text(now), ) + if idempotency_key is not None: + tx.insert_lease_request( + idempotency_key=idempotency_key, + intent_digest=intent_digest, + lease_id=lease.lease_id, + created_at=utc_text(now), + ) event_data: dict[str, Any] = { "lease_id": lease.lease_id, "artifact_id": artifact.artifact_id, @@ -1127,10 +1169,22 @@ def record_fenced_result(self, result: FencedResult) -> FencedReceipt | None: raise ConflictError("conflicting terminal target result", details=conflict) return derived_receipt + def get_lease(self, lease_id: str) -> Lease | None: + """Return one persisted lease, including terminal state, without mutating it.""" + _require_text("lease_id", lease_id) + with self.store.transaction(write=False) as tx: + return tx.get_lease(lease_id) + def get_fenced_attempt(self, attempt_id: str) -> FencedAttempt | None: with self.store.transaction(write=False) as tx: return tx.get_fenced_attempt(attempt_id) + def get_fenced_attempt_for_lease(self, lease_id: str) -> FencedAttempt | None: + """Return the durable target attempt, if any, for one lease.""" + _require_text("lease_id", lease_id) + with self.store.transaction(write=False) as tx: + return tx.get_fenced_attempt_for_lease(lease_id) + def get_fenced_receipt(self, attempt_id: str) -> FencedReceipt | None: with self.store.transaction(write=False) as tx: attempt = tx.get_fenced_attempt(attempt_id) @@ -1690,6 +1744,44 @@ def _protected_snapshot( return [by_key[key] for key in sorted(target_precondition.protected_observations)] +def _lease_request_intent_digest( + *, + artifact: ArtifactRef, + effect_digest: str, + target: str, + operation: str, + required_assurance: AssuranceLevel, + ttl: timedelta, + boot_epoch: str, + target_precondition: TargetPrecondition | None, +) -> str: + """Bind retry identity to caller intent without persisting opaque raw values.""" + precondition_descriptor_digest = None + if target_precondition is not None: + descriptor: dict[str, Any] = { + "adapter_id": target_precondition.adapter_id, + "mechanism": target_precondition.mechanism, + "expected_token": target_precondition.expected_token, + "protected_observations": sorted(target_precondition.protected_observations), + "replay_assurance": target_precondition.replay_assurance.value, + } + precondition_descriptor_digest = digest_json( + descriptor, domain="lease:precondition-descriptor:v1" + ) + ttl_microseconds = ttl.days * 86_400_000_000 + ttl.seconds * 1_000_000 + ttl.microseconds + intent: dict[str, Any] = { + "artifact": {"id": artifact.artifact_id, "revision": artifact.revision}, + "effect_digest": effect_digest, + "target": target, + "operation": operation, + "required_assurance": required_assurance.value, + "ttl_microseconds": ttl_microseconds, + "boot_epoch": boot_epoch, + "precondition_descriptor_digest": precondition_descriptor_digest, + } + return digest_json(intent, domain="lease:request-intent:v1") + + def digest_effect(effect: Digestable, *, operation: str, target: str) -> str: """Digest an effect with the single domain used by lease and adapter paths.""" _require_text("operation", operation) diff --git a/src/truthlease/integrations/__init__.py b/src/truthlease/integrations/__init__.py new file mode 100644 index 0000000..84239e2 --- /dev/null +++ b/src/truthlease/integrations/__init__.py @@ -0,0 +1,7 @@ +"""Optional framework integrations. + +Import integrations explicitly so the dependency-free TruthLease core stays +usable without an orchestration framework installed. +""" + +__all__: list[str] = [] diff --git a/src/truthlease/integrations/langgraph.py b/src/truthlease/integrations/langgraph.py new file mode 100644 index 0000000..260ca9c --- /dev/null +++ b/src/truthlease/integrations/langgraph.py @@ -0,0 +1,653 @@ +"""LangGraph persistence with TruthLease commit-time coherence checks. + +LangGraph remembers where a workflow paused. TruthLease independently decides +whether the referenced artifact and target precondition are still valid when +the workflow resumes. The graph checkpoints only references, digests, durable +identifiers, and stable decision codes; resolvers keep raw effects and target +tokens outside checkpoint storage. + +The node names and state fields in this module are a persistence contract for +paused workflows. Rename them only with an explicit checkpoint migration. +""" + +from __future__ import annotations + +import re +from collections.abc import Callable +from datetime import timedelta +from typing import Literal, TypedDict + +try: + from langgraph.checkpoint.base import BaseCheckpointSaver + from langgraph.graph import END, START, StateGraph + from langgraph.graph.state import CompiledStateGraph + from langgraph.types import interrupt +except ModuleNotFoundError as error: # pragma: no cover - exercised without the extra + if error.name is not None and error.name.startswith("langgraph"): + raise ImportError( + "LangGraph support is optional; install it with `pip install 'truthlease[langgraph]'`." + ) from error + raise + +from truthlease.canonical import Digestable, digest_json +from truthlease.engine import TruthLease, digest_effect +from truthlease.errors import TruthLeaseError +from truthlease.models import ( + ArtifactRef, + AssuranceEvidence, + AssuranceLevel, + FencedAttempt, + FencedAttemptStatus, + FencedReceipt, + Lease, + TargetPrecondition, +) +from truthlease.target import TargetAdapter, execute_fenced + +LANGGRAPH_STATE_VERSION = 1 +_DIGEST_PATTERN = re.compile(r"^[a-z0-9][a-z0-9_-]*:[A-Za-z0-9._~+/=-]+$") + +Phase = Literal[ + "awaiting_approval", + "approved", + "ready_to_execute", + "awaiting_recovery", + "committed", + "reconcile", +] + + +class StaleSafeState(TypedDict, total=False): + """Digest-only checkpoint contract for a stale-safe target effect.""" + + schema_version: int + idempotency_key: str + artifact_id: str + artifact_revision: int + effect_ref: str + effect_digest: str + precondition_ref: str + precondition_digest: str + target: str + operation: str + phase: Phase + approved: bool + recovery_approved: bool + recovery_basis_digest: str + lease_id: str + attempt_id: str + receipt_id: str + decision_code: str + reconciliation_cause: str + + +Revalidate = Callable[[TruthLease, ArtifactRef], None] +EffectResolver = Callable[[str], Digestable] +PreconditionResolver = Callable[[str], TargetPrecondition] +EvidenceProvider = Callable[[TruthLease, Lease], AssuranceEvidence] +StaleSafeGraph = CompiledStateGraph[ + StaleSafeState, + None, + StaleSafeState, + StaleSafeState, +] + + +def workflow_idempotency_key(stable_workflow_key: Digestable) -> str: + """Convert a stable caller key into the digest persisted by both stores.""" + return digest_json(stable_workflow_key, domain="langgraph:workflow-idempotency:v1") + + +def make_resume_state( + *, + idempotency_key: str, + artifact: ArtifactRef, + effect_ref: str, + effect: Digestable, + precondition_ref: str, + target_precondition: TargetPrecondition, + target: str, + operation: str, +) -> StaleSafeState: + """Create the only supported initial state without checkpointing raw inputs.""" + _require_digest("idempotency_key", idempotency_key) + for name, value in ( + ("effect_ref", effect_ref), + ("precondition_ref", precondition_ref), + ("target", target), + ("operation", operation), + ): + _require_text(name, value) + return StaleSafeState( + schema_version=LANGGRAPH_STATE_VERSION, + idempotency_key=idempotency_key, + artifact_id=artifact.artifact_id, + artifact_revision=artifact.revision, + effect_ref=effect_ref, + effect_digest=digest_effect(effect, operation=operation, target=target), + precondition_ref=precondition_ref, + precondition_digest=_precondition_descriptor_digest(target_precondition), + target=target, + operation=operation, + phase="awaiting_approval", + approved=False, + recovery_approved=False, + recovery_basis_digest="", + decision_code="AWAITING_APPROVAL", + ) + + +def build_stale_safe_graph( + runtime: TruthLease, + *, + checkpointer: BaseCheckpointSaver[str], + revalidate: Revalidate, + resolve_effect: EffectResolver, + resolve_precondition: PreconditionResolver, + evidence_provider: EvidenceProvider, + target_adapter: TargetAdapter, + lease_ttl: timedelta = timedelta(seconds=30), +) -> StaleSafeGraph: + """Compile a restart-safe approval and fenced-commit workflow. + + The supplied callbacks are deliberately replaceable. ``revalidate`` must + re-read authoritative sources and commit any changed observations before + returning. Resolvers must reconstruct raw values from references and are + invoked again immediately before target execution. + """ + if lease_ttl <= timedelta(0): + raise ValueError("lease_ttl must be positive") + + def approval(state: StaleSafeState) -> StaleSafeState: + failure = _state_failure(state, expected_phases={"awaiting_approval"}) + if failure is not None: + return failure + approved = interrupt( + { + "kind": "truthlease_effect_approval", + "schema_version": LANGGRAPH_STATE_VERSION, + "artifact_id": state["artifact_id"], + "artifact_revision": state["artifact_revision"], + "effect_digest": state["effect_digest"], + "precondition_digest": state["precondition_digest"], + "target": state["target"], + "operation": state["operation"], + } + ) + if type(approved) is not bool: + return _reconcile("APPROVAL_RESPONSE_INVALID", "approval") + if not approved: + return { + "approved": False, + **_reconcile("APPROVAL_DENIED", "approval"), + } + return { + "approved": True, + "phase": "approved", + "decision_code": "APPROVED_FOR_REVALIDATION", + "reconciliation_cause": "", + } + + def authorize(state: StaleSafeState) -> StaleSafeState: + failure = _state_failure(state, expected_phases={"approved"}) + if failure is not None: + return failure + artifact = ArtifactRef(state["artifact_id"], state["artifact_revision"]) + try: + revalidate(runtime, artifact) + except TruthLeaseError as error: + return _reconcile(error.code, "artifact") + except Exception: + return _reconcile("AUTHORITATIVE_REVALIDATION_FAILED", "artifact") + try: + effect, precondition = _resolve_and_verify( + state, + resolve_effect=resolve_effect, + resolve_precondition=resolve_precondition, + ) + except _ReferenceFailure as error: + return _reconcile(error.code, "resolver") + try: + lease = runtime.acquire_lease( + artifact=artifact, + effect=effect, + target=state["target"], + operation=state["operation"], + required_assurance=AssuranceLevel.FENCED, + ttl=lease_ttl, + target_precondition=precondition, + idempotency_key=state["idempotency_key"], + ) + except TruthLeaseError as error: + return _reconcile(error.code, "artifact") + except Exception: + return _reconcile("LEASE_ACQUISITION_FAILED", "lease") + return { + "lease_id": lease.lease_id, + "phase": "ready_to_execute", + "recovery_approved": False, + "recovery_basis_digest": "", + "decision_code": "FENCED_LEASE_ACQUIRED", + "reconciliation_cause": "", + } + + def execute(state: StaleSafeState) -> StaleSafeState: + failure = _state_failure(state, expected_phases={"ready_to_execute"}) + if failure is not None: + return failure + lease_id = state.get("lease_id") + if not isinstance(lease_id, str) or not lease_id: + return _reconcile("LEASE_REFERENCE_MISSING", "state") + try: + existing = runtime.get_fenced_attempt_for_lease(lease_id) + except TruthLeaseError as error: + return _reconcile(error.code, "state") + except Exception: + return _reconcile("ATTEMPT_STATE_READ_FAILED", "state") + ambiguous_statuses = { + FencedAttemptStatus.PREPARED, + FencedAttemptStatus.INDETERMINATE, + } + if existing is not None and existing.status not in ambiguous_statuses: + return _state_from_durable_attempt( + runtime, + existing, + missing_code="FENCED_ATTEMPT_MISSING", + missing_cause="state", + ) + if ( + existing is not None + and existing.status in ambiguous_statuses + and ( + state.get("recovery_approved") is not True + or state.get("recovery_basis_digest") != _attempt_recovery_basis(existing) + ) + ): + return { + "attempt_id": existing.attempt_id, + "phase": "awaiting_recovery", + "recovery_approved": False, + "recovery_basis_digest": _attempt_recovery_basis(existing), + "decision_code": "TARGET_OUTCOME_INDETERMINATE", + "reconciliation_cause": "target", + } + try: + effect, precondition = _resolve_and_verify( + state, + resolve_effect=resolve_effect, + resolve_precondition=resolve_precondition, + ) + except _ReferenceFailure as error: + return _reconcile(error.code, "resolver") + evidence: AssuranceEvidence | None = None + if existing is None: + try: + lease = runtime.get_lease(lease_id) + except TruthLeaseError as error: + return _reconcile(error.code, "state") + except Exception: + return _reconcile("LEASE_STATE_READ_FAILED", "state") + if lease is None: + return _reconcile("LEASE_NOT_FOUND", "state") + try: + evidence = evidence_provider(runtime, lease) + except Exception: + return _reconcile("EVIDENCE_PROVIDER_FAILED", "evidence") + try: + receipt = execute_fenced( + runtime, + target_adapter, + lease_id=lease_id, + effect=effect, + target_precondition=precondition, + evidence=evidence, + ) + except TruthLeaseError as error: + return _recover_after_execution_failure( + runtime, + lease_id=lease_id, + fallback_code=error.code, + fallback_cause="target", + ) + except Exception: + return _recover_after_execution_failure( + runtime, + lease_id=lease_id, + fallback_code="TARGET_EXECUTION_FAILED", + fallback_cause="target", + ) + if receipt is not None: + return _committed_state(receipt) + try: + attempt = runtime.get_fenced_attempt_for_lease(lease_id) + except Exception as error: + raise _PostExecutionStateUnavailable from error + return _state_from_durable_attempt( + runtime, + attempt, + missing_code="FENCED_ATTEMPT_MISSING", + missing_cause="state", + ) + + def recovery(state: StaleSafeState) -> StaleSafeState: + failure = _state_failure(state, expected_phases={"awaiting_recovery"}) + if failure is not None: + return failure + retry = interrupt( + { + "kind": "truthlease_recovery_decision", + "schema_version": LANGGRAPH_STATE_VERSION, + "artifact_id": state["artifact_id"], + "artifact_revision": state["artifact_revision"], + "effect_digest": state["effect_digest"], + "attempt_id": state.get("attempt_id", ""), + "recovery_basis_digest": state.get("recovery_basis_digest", ""), + "decision_code": "TARGET_OUTCOME_INDETERMINATE", + } + ) + lease_id = state.get("lease_id") + if not isinstance(lease_id, str) or not lease_id: + return _reconcile("LEASE_REFERENCE_MISSING", "state") + try: + attempt = runtime.get_fenced_attempt_for_lease(lease_id) + except TruthLeaseError as error: + return _reconcile(error.code, "target") + except Exception: + return _reconcile("RECOVERY_STATE_READ_FAILED", "target") + if attempt is None: + return _reconcile("FENCED_ATTEMPT_MISSING", "state") + if attempt.status not in { + FencedAttemptStatus.PREPARED, + FencedAttemptStatus.INDETERMINATE, + }: + return _state_from_durable_attempt( + runtime, + attempt, + missing_code="FENCED_ATTEMPT_MISSING", + missing_cause="state", + ) + current_basis = _attempt_recovery_basis(attempt) + if current_basis != state.get("recovery_basis_digest"): + return { + "phase": "ready_to_execute", + "recovery_approved": False, + "recovery_basis_digest": current_basis, + "decision_code": "RECOVERY_BASIS_CHANGED", + "reconciliation_cause": "target", + } + if type(retry) is not bool: + return _reconcile("RECOVERY_RESPONSE_INVALID", "target") + if not retry: + return _reconcile("RECOVERY_DECLINED", "target") + return { + "phase": "ready_to_execute", + "recovery_approved": True, + "recovery_basis_digest": current_basis, + "decision_code": "RECOVERY_RETRY_APPROVED", + "reconciliation_cause": "", + } + + builder = StateGraph(StaleSafeState) + builder.add_node("approval_v1", approval) + builder.add_node("authorize_v1", authorize) + builder.add_node("execute_fenced_v1", execute) + builder.add_node("recover_indeterminate_v1", recovery) + builder.add_edge(START, "approval_v1") + builder.add_conditional_edges( + "approval_v1", + _route_after_approval, + {"authorize": "authorize_v1", "end": END}, + ) + builder.add_conditional_edges( + "authorize_v1", + _route_after_authorize, + {"execute": "execute_fenced_v1", "end": END}, + ) + builder.add_conditional_edges( + "execute_fenced_v1", + _route_after_execute, + {"recover": "recover_indeterminate_v1", "end": END}, + ) + builder.add_conditional_edges( + "recover_indeterminate_v1", + _route_after_recovery, + {"execute": "execute_fenced_v1", "end": END}, + ) + return builder.compile(checkpointer=checkpointer) + + +class _ReferenceFailure(ValueError): + def __init__(self, code: str) -> None: + super().__init__(code) + self.code = code + + +class _PostExecutionStateUnavailable(RuntimeError): + """Keep the node retryable when post-dispatch durable state cannot be read.""" + + def __init__(self) -> None: + super().__init__( + "post-execution durable state is unavailable; retry this workflow identity" + ) + + +def _recover_after_execution_failure( + runtime: TruthLease, + *, + lease_id: str, + fallback_code: str, + fallback_cause: str, +) -> StaleSafeState: + """Prefer durable attempt truth over an exception raised after preparation.""" + try: + attempt = runtime.get_fenced_attempt_for_lease(lease_id) + except Exception as error: + raise _PostExecutionStateUnavailable from error + return _state_from_durable_attempt( + runtime, + attempt, + missing_code=fallback_code, + missing_cause=fallback_cause, + ) + + +def _state_from_durable_attempt( + runtime: TruthLease, + attempt: FencedAttempt | None, + *, + missing_code: str, + missing_cause: str, +) -> StaleSafeState: + if attempt is None: + return _reconcile(missing_code, missing_cause) + if attempt.status is FencedAttemptStatus.COMMITTED: + try: + receipt = runtime.get_fenced_receipt(attempt.attempt_id) + except Exception as error: + raise _PostExecutionStateUnavailable from error + if receipt is None: + return { + "attempt_id": attempt.attempt_id, + **_reconcile("FENCED_RECEIPT_MISSING", "state"), + } + return _committed_state(receipt) + if attempt.status is FencedAttemptStatus.NOT_COMMITTED: + return { + "attempt_id": attempt.attempt_id, + "recovery_approved": False, + "recovery_basis_digest": "", + **_reconcile("TARGET_PRECONDITION_REJECTED", "target"), + } + if attempt.status in { + FencedAttemptStatus.PREPARED, + FencedAttemptStatus.INDETERMINATE, + }: + return { + "attempt_id": attempt.attempt_id, + "phase": "awaiting_recovery", + "recovery_approved": False, + "recovery_basis_digest": _attempt_recovery_basis(attempt), + "decision_code": "TARGET_OUTCOME_INDETERMINATE", + "reconciliation_cause": "target", + } + return { + "attempt_id": attempt.attempt_id, + **_reconcile("FENCED_ATTEMPT_NOT_COMMITTABLE", "target"), + } + + +def _committed_state(receipt: FencedReceipt) -> StaleSafeState: + return { + "attempt_id": receipt.attempt_id, + "receipt_id": receipt.receipt_id, + "phase": "committed", + "recovery_approved": False, + "recovery_basis_digest": "", + "decision_code": "FENCED_COMMIT_RECORDED", + "reconciliation_cause": "", + } + + +def _resolve_and_verify( + state: StaleSafeState, + *, + resolve_effect: EffectResolver, + resolve_precondition: PreconditionResolver, +) -> tuple[Digestable, TargetPrecondition]: + try: + effect = resolve_effect(state["effect_ref"]) + except Exception as error: + raise _ReferenceFailure("EFFECT_REFERENCE_UNAVAILABLE") from error + try: + resolved_effect_digest = digest_effect( + effect, + operation=state["operation"], + target=state["target"], + ) + except (TypeError, ValueError) as error: + raise _ReferenceFailure("EFFECT_REFERENCE_INVALID") from error + if resolved_effect_digest != state["effect_digest"]: + raise _ReferenceFailure("EFFECT_REFERENCE_CHANGED") + try: + precondition = resolve_precondition(state["precondition_ref"]) + except Exception as error: + raise _ReferenceFailure("PRECONDITION_REFERENCE_UNAVAILABLE") from error + if not isinstance(precondition, TargetPrecondition): + raise _ReferenceFailure("PRECONDITION_REFERENCE_INVALID") + if _precondition_descriptor_digest(precondition) != state["precondition_digest"]: + raise _ReferenceFailure("PRECONDITION_REFERENCE_CHANGED") + return effect, precondition + + +def _precondition_descriptor_digest(precondition: TargetPrecondition) -> str: + return digest_json( + { + "adapter_id": precondition.adapter_id, + "mechanism": precondition.mechanism, + "expected_token": precondition.expected_token, + "protected_observations": sorted(precondition.protected_observations), + "replay_assurance": precondition.replay_assurance.value, + }, + domain="langgraph:precondition-descriptor:v1", + ) + + +def _attempt_recovery_basis(attempt: FencedAttempt) -> str: + return digest_json( + { + "attempt_id": attempt.attempt_id, + "status": attempt.status.value, + "result_digest": attempt.result_digest, + }, + domain="langgraph:recovery-basis:v1", + ) + + +def _state_failure( + state: StaleSafeState, + *, + expected_phases: set[str], +) -> StaleSafeState | None: + try: + if state.get("schema_version") != LANGGRAPH_STATE_VERSION: + raise ValueError + if state.get("phase") not in expected_phases: + raise ValueError + if state.get("phase") != "awaiting_approval" and state.get("approved") is not True: + raise ValueError + if type(state.get("recovery_approved")) is not bool: + raise ValueError + recovery_basis = state.get("recovery_basis_digest") + if not isinstance(recovery_basis, str): + raise ValueError + if state.get("phase") == "awaiting_recovery" and not _DIGEST_PATTERN.fullmatch( + recovery_basis + ): + raise ValueError + if state.get("recovery_approved") is True and not _DIGEST_PATTERN.fullmatch(recovery_basis): + raise ValueError + if not isinstance(state.get("artifact_revision"), int) or state["artifact_revision"] <= 0: + raise ValueError + for key in ( + "artifact_id", + "effect_ref", + "precondition_ref", + "target", + "operation", + ): + value = state.get(key) + if not isinstance(value, str) or not value.strip(): + raise ValueError + for key in ("idempotency_key", "effect_digest", "precondition_digest"): + value = state.get(key) + if not isinstance(value, str) or not _DIGEST_PATTERN.fullmatch(value): + raise ValueError + except (KeyError, TypeError, ValueError): + return _reconcile("CHECKPOINT_STATE_INVALID", "state") + return None + + +def _reconcile(code: str, cause: str) -> StaleSafeState: + return { + "phase": "reconcile", + "recovery_approved": False, + "recovery_basis_digest": "", + "decision_code": code, + "reconciliation_cause": cause, + } + + +def _route_after_approval(state: StaleSafeState) -> Literal["authorize", "end"]: + return "authorize" if state.get("phase") == "approved" else "end" + + +def _route_after_authorize(state: StaleSafeState) -> Literal["execute", "end"]: + return "execute" if state.get("phase") == "ready_to_execute" else "end" + + +def _route_after_execute(state: StaleSafeState) -> Literal["recover", "end"]: + return "recover" if state.get("phase") == "awaiting_recovery" else "end" + + +def _route_after_recovery(state: StaleSafeState) -> Literal["execute", "end"]: + return "execute" if state.get("phase") == "ready_to_execute" else "end" + + +def _require_text(name: str, value: str) -> None: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{name} must be a non-empty string") + + +def _require_digest(name: str, value: str) -> None: + if not isinstance(value, str) or not _DIGEST_PATTERN.fullmatch(value): + raise ValueError(f"{name} must be an algorithm-prefixed digest") + + +__all__ = [ + "LANGGRAPH_STATE_VERSION", + "StaleSafeGraph", + "StaleSafeState", + "build_stale_safe_graph", + "make_resume_state", + "workflow_idempotency_key", +] diff --git a/src/truthlease/storage/sqlite.py b/src/truthlease/storage/sqlite.py index 53c084e..23df892 100644 --- a/src/truthlease/storage/sqlite.py +++ b/src/truthlease/storage/sqlite.py @@ -39,7 +39,7 @@ ReplayAssurance, ) -SCHEMA_VERSION = 2 +SCHEMA_VERSION = 3 APPLICATION_ID = 0x54525554 # ASCII "TRUT" _V1_EXPECTED_TABLES = { "artifacts", @@ -140,13 +140,13 @@ "dependencies_reverse_observation", "leases_by_artifact", } -_EXPECTED_TABLES = _V1_EXPECTED_TABLES | { +_V2_EXPECTED_TABLES = _V1_EXPECTED_TABLES | { "lease_fence_bindings", "fenced_attempts", "fenced_results", "fenced_receipts", } -_EXPECTED_COLUMNS = { +_V2_EXPECTED_COLUMNS = { **_V1_EXPECTED_COLUMNS, "lease_fence_bindings": { "lease_id", @@ -218,11 +218,22 @@ "guarantee_text", }, } -_EXPECTED_INDEXES = _V1_EXPECTED_INDEXES | { +_V2_EXPECTED_INDEXES = _V1_EXPECTED_INDEXES | { "fenced_attempts_by_status", "fenced_results_by_attempt", "fenced_receipts_by_lease", } +_EXPECTED_TABLES = _V2_EXPECTED_TABLES | {"lease_requests"} +_EXPECTED_COLUMNS = { + **_V2_EXPECTED_COLUMNS, + "lease_requests": { + "idempotency_key", + "intent_digest", + "lease_id", + "created_at", + }, +} +_EXPECTED_INDEXES = _V2_EXPECTED_INDEXES class SQLiteStore: @@ -257,15 +268,18 @@ def initialize(self) -> None: raise RuntimeError( f"database belongs to application_id {application_id}, not TruthLease" ) - if version not in (0, 1, SCHEMA_VERSION): + if version not in (0, 1, 2, SCHEMA_VERSION): raise RuntimeError( - f"unsupported database schema version {version}; expected 1 or {SCHEMA_VERSION}" + "unsupported database schema version " + f"{version}; expected 1, 2, or {SCHEMA_VERSION}" ) if version == 0 and (application_id != 0 or existing_tables): raise RuntimeError("refusing to initialize a non-empty unversioned database") - if version in (1, SCHEMA_VERSION) and application_id != APPLICATION_ID: + if version in (1, 2, SCHEMA_VERSION) and application_id != APPLICATION_ID: raise RuntimeError("versioned database is missing the TruthLease application_id") - if version in (1, SCHEMA_VERSION) and not _schema_shape_ok(connection, version=version): + if version in (1, 2, SCHEMA_VERSION) and not _schema_shape_ok( + connection, version=version + ): raise RuntimeError("TruthLease database schema shape is incomplete or unexpected") _configure_owned_connection(connection) connection.execute("BEGIN IMMEDIATE") @@ -279,6 +293,10 @@ def initialize(self) -> None: ) elif version == 1: _execute_schema(connection, _SCHEMA_V2_ADDITIONS) + _execute_schema(connection, _SCHEMA_V3_ADDITIONS) + connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}") + elif version == 2: + _execute_schema(connection, _SCHEMA_V3_ADDITIONS) connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}") connection.commit() except BaseException: @@ -690,6 +708,32 @@ def invalidate_leases(self, lease_ids: Sequence[str]) -> None: ], ) + def get_lease_request(self, idempotency_key: str) -> tuple[str, str] | None: + """Return the intent digest and lease bound to one digest-only request key.""" + row = self.connection.execute( + "SELECT intent_digest, lease_id FROM lease_requests WHERE idempotency_key = ?", + (idempotency_key,), + ).fetchone() + if row is None: + return None + return str(row["intent_digest"]), str(row["lease_id"]) + + def insert_lease_request( + self, + *, + idempotency_key: str, + intent_digest: str, + lease_id: str, + created_at: str, + ) -> None: + self.connection.execute( + """ + INSERT INTO lease_requests(idempotency_key, intent_digest, lease_id, created_at) + VALUES (?, ?, ?, ?) + """, + (idempotency_key, intent_digest, lease_id, created_at), + ) + def insert_lease(self, lease: Lease) -> None: self.connection.execute( """ @@ -1036,6 +1080,10 @@ def _schema_shape_ok(connection: sqlite3.Connection, *, version: int) -> bool: expected_tables = _V1_EXPECTED_TABLES expected_columns = _V1_EXPECTED_COLUMNS expected_indexes = _V1_EXPECTED_INDEXES + elif version == 2: + expected_tables = _V2_EXPECTED_TABLES + expected_columns = _V2_EXPECTED_COLUMNS + expected_indexes = _V2_EXPECTED_INDEXES elif version == SCHEMA_VERSION: expected_tables = _EXPECTED_TABLES expected_columns = _EXPECTED_COLUMNS @@ -1404,7 +1452,19 @@ def _fenced_receipt_from_row(row: sqlite3.Row) -> FencedReceipt: ON fenced_receipts(lease_id); """ -_SCHEMA = _SCHEMA_V1 + _SCHEMA_V2_ADDITIONS + +_SCHEMA_V3_ADDITIONS = """ +CREATE TABLE IF NOT EXISTS lease_requests ( + idempotency_key TEXT PRIMARY KEY, + intent_digest TEXT NOT NULL, + lease_id TEXT NOT NULL UNIQUE, + created_at TEXT NOT NULL, + FOREIGN KEY(lease_id) REFERENCES leases(lease_id) +); +""" + + +_SCHEMA = _SCHEMA_V1 + _SCHEMA_V2_ADDITIONS + _SCHEMA_V3_ADDITIONS def _execute_schema(connection: sqlite3.Connection, schema: str = _SCHEMA) -> None: diff --git a/tests/test_cli_demo_storage.py b/tests/test_cli_demo_storage.py index 19e3e42..a943e9b 100644 --- a/tests/test_cli_demo_storage.py +++ b/tests/test_cli_demo_storage.py @@ -8,7 +8,7 @@ from truthlease.cli import main from truthlease.demo import run_demo -from truthlease.storage.sqlite import SQLiteStore +from truthlease.storage.sqlite import SCHEMA_VERSION, SQLiteStore def test_demo_proves_partial_reuse_and_store_integrity(tmp_path: Path) -> None: @@ -114,7 +114,7 @@ def test_database_integrity_contract(tmp_path: Path) -> None: assert report["ok"] is True assert report["foreign_keys_enabled"] is True assert report["journal_mode"] == "delete" - assert report["schema_version"] == 2 + assert report["schema_version"] == SCHEMA_VERSION == 3 def test_cli_exposes_and_enforces_observation_generation( diff --git a/tests/test_langgraph_integration.py b/tests/test_langgraph_integration.py new file mode 100644 index 0000000..2f15aa0 --- /dev/null +++ b/tests/test_langgraph_integration.py @@ -0,0 +1,1041 @@ +from __future__ import annotations + +import sqlite3 +from dataclasses import dataclass, field +from datetime import UTC, datetime, timedelta +from pathlib import Path +from typing import cast + +import pytest +from conftest import ManualClock, SequentialIds +from langchain_core.runnables import RunnableConfig +from langgraph.checkpoint.sqlite import SqliteSaver +from langgraph.types import Command + +import truthlease.integrations.langgraph as langgraph_integration +from truthlease.canonical import Digestable, digest_json +from truthlease.engine import TruthLease +from truthlease.integrations.langgraph import ( + StaleSafeGraph, + StaleSafeState, + build_stale_safe_graph, + make_resume_state, + workflow_idempotency_key, +) +from truthlease.models import ( + ArtifactDependency, + ArtifactRef, + AssuranceEvidence, + AssuranceLevel, + DependencyCompleteness, + FencedAttempt, + FencedCommitRequest, + FencedOutcome, + FencedResult, + Lease, + ReplayAssurance, + TargetPrecondition, +) +from truthlease.storage.sqlite import SQLiteStore +from truthlease.target import execute_fenced + +_EFFECT_SECRET = "raw-effect-must-never-be-checkpointed" +_TOKEN_SECRET = "raw-etag-must-never-be-checkpointed" +_TARGET = "urn:example:orders" +_OPERATION = "create-order" + + +@dataclass +class _TargetStore: + current_token: str = _TOKEN_SECRET + committed: dict[str, FencedResult] = field(default_factory=dict) + effect_count: int = 0 + dispatch_count: int = 0 + lose_response_once: bool = False + hard_crash_after_commit_once: bool = False + + +@dataclass +class _DeduplicatingTarget: + store: _TargetStore + adapter_id: str = "adapter:test" + + def commit(self, request: FencedCommitRequest) -> FencedResult: + self.store.dispatch_count += 1 + existing = self.store.committed.get(request.attempt_id) + if existing is not None: + return existing + if request.precondition.expected_token != self.store.current_token: + return _target_result(request, outcome=FencedOutcome.NOT_COMMITTED) + + self.store.effect_count += 1 + result = _target_result(request, outcome=FencedOutcome.COMMITTED) + self.store.committed[request.attempt_id] = result + if self.store.hard_crash_after_commit_once: + self.store.hard_crash_after_commit_once = False + raise SystemExit("simulated process death after target commit") + if self.store.lose_response_once: + self.store.lose_response_once = False + raise TimeoutError("simulated response loss after target commit") + return result + + +def _target_result( + request: FencedCommitRequest, + *, + outcome: FencedOutcome, +) -> FencedResult: + committed = outcome is FencedOutcome.COMMITTED + return FencedResult( + attempt_id=request.attempt_id, + lease_id=request.lease_id, + adapter_id=request.adapter_id, + outcome=outcome, + request_digest=request.request_digest, + closure_digest=request.closure_digest, + effect_digest=request.effect_digest, + target=request.target, + operation=request.operation, + precondition_digest=request.precondition_digest, + scope_digest=request.scope_digest, + replay_assurance=request.precondition.replay_assurance, + coverage=request.coverage, + target_result_digest=digest_json( + {"attempt_id": request.attempt_id, "outcome": outcome.value}, + domain="test:langgraph-target-result", + ), + target_receipt_ref=f"target-receipt:{request.attempt_id}" if committed else None, + committed_at=datetime(2026, 7, 22, 12, 0, tzinfo=UTC) if committed else None, + ) + + +class _Harness: + def __init__( + self, + db_path: Path, + clock: ManualClock, + *, + completeness: DependencyCompleteness = DependencyCompleteness.OBSERVED, + ) -> None: + self.db_path = db_path + self.clock = clock + self.runtime = self._new_runtime(ids=SequentialIds()) + self.authoritative_policy_version = "v1" + self.revalidation_failure = False + self.effect_ref = "effect/order/v1" + self.precondition_ref = "precondition/orders/v1" + self.effects: dict[str, Digestable] = { + self.effect_ref: {"payload": _EFFECT_SECRET, "write": True} + } + self.preconditions = { + self.precondition_ref: TargetPrecondition( + adapter_id="adapter:test", + mechanism="strong-etag", + expected_token=_TOKEN_SECRET, + protected_observations=("policy",), + replay_assurance=ReplayAssurance.DEDUPLICATED, + ) + } + self.target_store = _TargetStore() + self.evidence_calls = 0 + self.evidence_failure = False + self.runtime.observe(key="policy", version="v1", value={"enabled": True}) + artifact = self.runtime.publish_artifact( + artifact_id="plan", + kind="plan", + payload={"step": "create-order"}, + dependencies=[ArtifactDependency.observation(self.runtime.get_observation("policy"))], + completeness=completeness, + ) + self.artifact = artifact.ref + self.idempotency_key = workflow_idempotency_key({"workflow": "order-42"}) + + def _new_runtime(self, *, ids: SequentialIds | None = None) -> TruthLease: + return TruthLease( + SQLiteStore(self.db_path), + clock=self.clock, + ids=ids, + trusted_evidence_authorities={"evidence:test"}, + trusted_target_adapters={"adapter:test"}, + ) + + def restart_runtime(self) -> None: + self.runtime = self._new_runtime() + + def revalidate(self, runtime: TruthLease, _artifact: ArtifactRef) -> None: + if self.revalidation_failure: + raise RuntimeError("simulated authoritative source failure") + observed = runtime.get_observation("policy") + if observed.version != self.authoritative_policy_version: + runtime.observe( + key="policy", + version=self.authoritative_policy_version, + value={"enabled": self.authoritative_policy_version == "v1"}, + expected_generation=observed.generation, + ) + + def resolve_effect(self, reference: str) -> Digestable: + return self.effects[reference] + + def resolve_precondition(self, reference: str) -> TargetPrecondition: + return self.preconditions[reference] + + def evidence(self, runtime: TruthLease, lease: Lease) -> AssuranceEvidence: + self.evidence_calls += 1 + if self.evidence_failure: + raise RuntimeError("simulated evidence provider failure") + return AssuranceEvidence( + level=AssuranceLevel.REVALIDATED, + authority="evidence:test", + evidence_digest=digest_json( + {"lease_id": lease.lease_id, "closure": lease.closure_digest}, + domain="test:langgraph-evidence", + ), + observed_at=runtime.clock.now(), + lease_id=lease.lease_id, + closure_digest=lease.closure_digest, + effect_digest=lease.effect_digest, + target=lease.target, + operation=lease.operation, + ) + + def graph(self, saver: SqliteSaver) -> StaleSafeGraph: + return build_stale_safe_graph( + self.runtime, + checkpointer=saver, + revalidate=self.revalidate, + resolve_effect=self.resolve_effect, + resolve_precondition=self.resolve_precondition, + evidence_provider=self.evidence, + target_adapter=_DeduplicatingTarget(self.target_store), + ) + + def state( + self, + *, + effect_ref: str | None = None, + idempotency_key: str | None = None, + ) -> StaleSafeState: + resolved_effect_ref = effect_ref or self.effect_ref + return make_resume_state( + idempotency_key=idempotency_key or self.idempotency_key, + artifact=self.artifact, + effect_ref=resolved_effect_ref, + effect=self.effects[resolved_effect_ref], + precondition_ref=self.precondition_ref, + target_precondition=self.preconditions[self.precondition_ref], + target=_TARGET, + operation=_OPERATION, + ) + + +def _config(thread_id: str) -> RunnableConfig: + return {"configurable": {"thread_id": thread_id}} + + +def _connection(path: Path) -> sqlite3.Connection: + return sqlite3.connect(path, check_same_thread=False) + + +def _restart_failed_node( + graph: StaleSafeGraph, + config: RunnableConfig, +) -> StaleSafeState: + return cast(StaleSafeState, graph.invoke(cast(StaleSafeState, None), config)) + + +def test_persistent_restart_resumes_to_one_fenced_commit_and_redacts_raw_values( + tmp_path: Path, + clock: ManualClock, +) -> None: + truth_db = tmp_path / "truthlease.db" + checkpoint_db = tmp_path / "checkpoints.db" + harness = _Harness(truth_db, clock) + config = _config(harness.idempotency_key) + + first_connection = _connection(checkpoint_db) + paused = harness.graph(SqliteSaver(first_connection)).invoke(harness.state(), config) + assert "__interrupt__" in paused + assert paused["phase"] == "awaiting_approval" + first_connection.close() + + harness.restart_runtime() + second_connection = _connection(checkpoint_db) + committed = harness.graph(SqliteSaver(second_connection)).invoke(Command(resume=True), config) + assert committed["phase"] == "committed" + assert committed["decision_code"] == "FENCED_COMMIT_RECORDED" + assert harness.target_store.effect_count == 1 + assert harness.evidence_calls == 1 + second_connection.close() + + persisted = truth_db.read_bytes() + checkpoint_db.read_bytes() + assert _EFFECT_SECRET.encode() not in persisted + assert _TOKEN_SECRET.encode() not in persisted + + +def test_policy_change_while_paused_fails_closed_before_target_effect( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("policy-change") + + graph.invoke(harness.state(), config) + harness.authoritative_policy_version = "v2" + result = graph.invoke(Command(resume=True), config) + + assert result["phase"] == "reconcile" + assert result["decision_code"] == "ARTIFACT_NOT_FRESH" + assert harness.target_store.effect_count == 0 + assert harness.target_store.dispatch_count == 0 + connection.close() + + +def test_target_token_change_is_not_committed( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("target-token-change") + + graph.invoke(harness.state(), config) + harness.target_store.current_token = "new-target-etag" + result = graph.invoke(Command(resume=True), config) + + assert result["phase"] == "reconcile" + assert result["decision_code"] == "TARGET_PRECONDITION_REJECTED" + assert harness.target_store.effect_count == 0 + assert harness.target_store.dispatch_count == 1 + connection.close() + + +def test_duplicate_workflow_dispatch_reuses_terminal_attempt_without_duplicate_effect( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + + graph.invoke(harness.state(), _config("first-dispatch")) + first = graph.invoke(Command(resume=True), _config("first-dispatch")) + graph.invoke(harness.state(), _config("duplicate-dispatch")) + duplicate = graph.invoke(Command(resume=True), _config("duplicate-dispatch")) + + assert first["phase"] == duplicate["phase"] == "committed" + assert first["lease_id"] == duplicate["lease_id"] + assert first["attempt_id"] == duplicate["attempt_id"] + assert first["receipt_id"] == duplicate["receipt_id"] + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 1 + assert harness.evidence_calls == 1 + connection.close() + + +def test_lost_response_restart_recovers_same_attempt_without_duplicate_effect( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + checkpoint_db = tmp_path / "checkpoints.db" + config = _config("lost-response") + harness.target_store.lose_response_once = True + + first_connection = _connection(checkpoint_db) + graph = harness.graph(SqliteSaver(first_connection)) + graph.invoke(harness.state(), config) + uncertain = graph.invoke(Command(resume=True), config) + assert uncertain["phase"] == "awaiting_recovery" + assert uncertain["decision_code"] == "TARGET_OUTCOME_INDETERMINATE" + assert "__interrupt__" in uncertain + attempt_id = uncertain["attempt_id"] + first_connection.close() + + harness.restart_runtime() + second_connection = _connection(checkpoint_db) + recovered = harness.graph(SqliteSaver(second_connection)).invoke(Command(resume=True), config) + assert recovered["phase"] == "committed" + assert recovered["attempt_id"] == attempt_id + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 2 + assert harness.evidence_calls == 1 + second_connection.close() + + +def test_result_persistence_failure_after_commit_enters_same_attempt_recovery( + tmp_path: Path, + clock: ManualClock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("result-persistence-failure") + original_record = harness.runtime.record_fenced_result + + graph.invoke(harness.state(), config) + + def fail_result_persistence(_result: FencedResult) -> None: + raise OSError("simulated local result persistence failure") + + monkeypatch.setattr(harness.runtime, "record_fenced_result", fail_result_persistence) + uncertain = graph.invoke(Command(resume=True), config) + + assert uncertain["phase"] == "awaiting_recovery" + assert uncertain["decision_code"] == "TARGET_OUTCOME_INDETERMINATE" + assert "simulated" not in repr(uncertain) + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 1 + attempt = harness.runtime.get_fenced_attempt_for_lease(uncertain["lease_id"]) + assert attempt is not None + assert attempt.status.value == "prepared" + + monkeypatch.setattr(harness.runtime, "record_fenced_result", original_record) + committed = graph.invoke(Command(resume=True), config) + + assert committed["phase"] == "committed" + assert committed["attempt_id"] == attempt.attempt_id + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 2 + connection.close() + + +def test_persisted_receipt_returns_without_redundant_attempt_read( + tmp_path: Path, + clock: ManualClock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("receipt-short-circuit") + original_get = harness.runtime.get_fenced_attempt_for_lease + reads = 0 + + def fail_redundant_read(lease_id: str) -> FencedAttempt | None: + nonlocal reads + reads += 1 + if reads > 1: + raise OSError("unexpected read after persisted receipt") + return original_get(lease_id) + + graph.invoke(harness.state(), config) + monkeypatch.setattr( + harness.runtime, + "get_fenced_attempt_for_lease", + fail_redundant_read, + ) + committed = graph.invoke(Command(resume=True), config) + + assert committed["phase"] == "committed" + assert committed["decision_code"] == "FENCED_COMMIT_RECORDED" + assert reads == 1 + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 1 + connection.close() + + +def test_post_execution_state_read_failure_remains_retryable_and_redacted( + tmp_path: Path, + clock: ManualClock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("post-execution-state-unavailable") + original_get = harness.runtime.get_fenced_attempt_for_lease + reads = 0 + + def fail_result_persistence(_result: FencedResult) -> None: + raise OSError("sensitive persistence detail") + + def fail_post_execution_read(lease_id: str) -> FencedAttempt | None: + nonlocal reads + reads += 1 + if reads > 1: + raise OSError("sensitive state-read detail") + return original_get(lease_id) + + graph.invoke(harness.state(), config) + monkeypatch.setattr(harness.runtime, "record_fenced_result", fail_result_persistence) + monkeypatch.setattr( + harness.runtime, + "get_fenced_attempt_for_lease", + fail_post_execution_read, + ) + + with pytest.raises( + RuntimeError, + match="post-execution durable state is unavailable; retry this workflow identity", + ) as captured: + graph.invoke(Command(resume=True), config) + + assert "sensitive" not in str(captured.value) + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 1 + connection.close() + + +def test_execution_exception_recovers_an_already_committed_durable_receipt( + tmp_path: Path, + clock: ManualClock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + + graph.invoke(harness.state(), _config("original-commit")) + original = graph.invoke(Command(resume=True), _config("original-commit")) + assert original["phase"] == "committed" + + def fail_before_receipt_return(*_args: object, **_kwargs: object) -> None: + raise OSError("simulated helper failure") + + monkeypatch.setattr( + langgraph_integration, + "execute_fenced", + fail_before_receipt_return, + ) + graph.invoke(harness.state(), _config("committed-recovery")) + recovered = graph.invoke(Command(resume=True), _config("committed-recovery")) + + assert recovered["phase"] == "committed" + assert recovered["attempt_id"] == original["attempt_id"] + assert recovered["receipt_id"] == original["receipt_id"] + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 1 + connection.close() + + +def test_execution_failure_before_attempt_has_stable_terminal_code( + tmp_path: Path, + clock: ManualClock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("pre-attempt-target-failure") + + def fail_before_prepare(*_args: object, **_kwargs: object) -> None: + raise OSError("sensitive target detail") + + graph.invoke(harness.state(), config) + monkeypatch.setattr(langgraph_integration, "execute_fenced", fail_before_prepare) + failed = graph.invoke(Command(resume=True), config) + + assert failed["phase"] == "reconcile" + assert failed["decision_code"] == "TARGET_EXECUTION_FAILED" + assert "sensitive" not in repr(failed) + assert harness.target_store.dispatch_count == 0 + connection.close() + + +@pytest.mark.parametrize("recovery_response", [True, False, "invalid"]) +def test_newer_terminal_truth_overrides_a_stale_recovery_response( + tmp_path: Path, + clock: ManualClock, + recovery_response: object, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("recovery-basis-change") + harness.target_store.lose_response_once = True + + graph.invoke(harness.state(), config) + uncertain = graph.invoke(Command(resume=True), config) + assert uncertain["phase"] == "awaiting_recovery" + original_basis = uncertain["recovery_basis_digest"] + + resolved_receipt = execute_fenced( + harness.runtime, + _DeduplicatingTarget(harness.target_store), + lease_id=uncertain["lease_id"], + effect=harness.effects[harness.effect_ref], + target_precondition=harness.preconditions[harness.precondition_ref], + ) + assert resolved_receipt is not None + assert harness.target_store.dispatch_count == 2 + del harness.effects[harness.effect_ref] + + committed = graph.invoke(Command(resume=recovery_response), config) + assert committed["phase"] == "committed" + assert committed["recovery_approved"] is False + assert committed["recovery_basis_digest"] == "" + assert original_basis != "" + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 2 + connection.close() + + +def test_changed_ambiguous_result_requires_a_new_recovery_decision( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + harness.target_store.lose_response_once = True + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("changed-ambiguous-result") + + graph.invoke(harness.state(), config) + uncertain = graph.invoke(Command(resume=True), config) + original_basis = uncertain["recovery_basis_digest"] + attempt_id = uncertain["attempt_id"] + target_result = harness.target_store.committed[attempt_id] + changed_result = FencedResult( + attempt_id=target_result.attempt_id, + lease_id=target_result.lease_id, + adapter_id=target_result.adapter_id, + outcome=FencedOutcome.INDETERMINATE, + request_digest=target_result.request_digest, + closure_digest=target_result.closure_digest, + effect_digest=target_result.effect_digest, + target=target_result.target, + operation=target_result.operation, + precondition_digest=target_result.precondition_digest, + scope_digest=target_result.scope_digest, + replay_assurance=target_result.replay_assurance, + coverage=target_result.coverage, + target_result_digest=digest_json( + {"attempt_id": attempt_id, "evidence": "changed"}, + domain="test:changed-ambiguous-result", + ), + ) + harness.runtime.record_fenced_result(changed_result) + + reprompted = graph.invoke(Command(resume=True), config) + + assert reprompted["phase"] == "awaiting_recovery" + assert reprompted["decision_code"] == "TARGET_OUTCOME_INDETERMINATE" + assert reprompted["recovery_basis_digest"] != original_basis + assert "__interrupt__" in reprompted + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 1 + connection.close() + + +def test_crash_after_indeterminate_record_requires_recovery_before_redispatch( + tmp_path: Path, + clock: ManualClock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + checkpoint_db = tmp_path / "checkpoints.db" + config = _config("crash-after-indeterminate-record") + harness.target_store.lose_response_once = True + connection = _connection(checkpoint_db) + graph = harness.graph(SqliteSaver(connection)) + graph.invoke(harness.state(), config) + + original_get = harness.runtime.get_fenced_attempt_for_lease + calls = 0 + + def crash_on_post_dispatch_read(lease_id: str) -> FencedAttempt | None: + nonlocal calls + calls += 1 + if calls == 2: + raise SystemExit("simulated process death before graph checkpoint") + return original_get(lease_id) + + monkeypatch.setattr( + harness.runtime, + "get_fenced_attempt_for_lease", + crash_on_post_dispatch_read, + ) + with pytest.raises(SystemExit, match="before graph checkpoint"): + graph.invoke(Command(resume=True), config) + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 1 + connection.close() + + monkeypatch.setattr(harness.runtime, "get_fenced_attempt_for_lease", original_get) + harness.restart_runtime() + restarted_connection = _connection(checkpoint_db) + restarted = harness.graph(SqliteSaver(restarted_connection)) + gated = _restart_failed_node(restarted, config) + + assert gated["phase"] == "awaiting_recovery" + assert gated["decision_code"] == "TARGET_OUTCOME_INDETERMINATE" + assert gated["recovery_approved"] is False + assert "__interrupt__" in gated + assert harness.target_store.dispatch_count == 1 + + committed = restarted.invoke(Command(resume=True), config) + assert committed["phase"] == "committed" + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 2 + restarted_connection.close() + + +def test_crash_after_target_commit_before_local_record_requires_recovery_gate( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + checkpoint_db = tmp_path / "checkpoints.db" + config = _config("crash-after-target-commit") + harness.target_store.hard_crash_after_commit_once = True + connection = _connection(checkpoint_db) + graph = harness.graph(SqliteSaver(connection)) + graph.invoke(harness.state(), config) + + with pytest.raises(SystemExit, match="after target commit"): + graph.invoke(Command(resume=True), config) + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 1 + connection.close() + + harness.restart_runtime() + restarted_connection = _connection(checkpoint_db) + restarted = harness.graph(SqliteSaver(restarted_connection)) + gated = _restart_failed_node(restarted, config) + + assert gated["phase"] == "awaiting_recovery" + assert gated["decision_code"] == "TARGET_OUTCOME_INDETERMINATE" + assert gated["recovery_approved"] is False + assert "__interrupt__" in gated + assert harness.target_store.dispatch_count == 1 + + committed = restarted.invoke(Command(resume=True), config) + assert committed["phase"] == "committed" + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 2 + restarted_connection.close() + + +def test_unknown_dependency_completeness_never_reaches_target( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness( + tmp_path / "truthlease.db", + clock, + completeness=DependencyCompleteness.UNKNOWN, + ) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("unknown-completeness") + + graph.invoke(harness.state(), config) + result = graph.invoke(Command(resume=True), config) + + assert result["phase"] == "reconcile" + assert result["decision_code"] == "DEPENDENCY_COMPLETENESS_INSUFFICIENT" + assert harness.target_store.effect_count == 0 + assert harness.target_store.dispatch_count == 0 + connection.close() + + +def test_reference_change_after_approval_fails_before_lease_or_target( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("effect-change") + + graph.invoke(harness.state(), config) + harness.effects[harness.effect_ref] = {"payload": "changed-after-approval", "write": True} + result = graph.invoke(Command(resume=True), config) + + assert result["phase"] == "reconcile" + assert result["decision_code"] == "EFFECT_REFERENCE_CHANGED" + assert harness.target_store.effect_count == 0 + assert harness.target_store.dispatch_count == 0 + connection.close() + + +def test_missing_effect_reference_has_distinct_redacted_code( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("missing-effect-reference") + + graph.invoke(harness.state(), config) + del harness.effects[harness.effect_ref] + result = graph.invoke(Command(resume=True), config) + + assert result["phase"] == "reconcile" + assert result["decision_code"] == "EFFECT_REFERENCE_UNAVAILABLE" + assert harness.target_store.dispatch_count == 0 + connection.close() + + +def test_missing_precondition_reference_has_distinct_redacted_code( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("missing-precondition-reference") + + graph.invoke(harness.state(), config) + del harness.preconditions[harness.precondition_ref] + result = graph.invoke(Command(resume=True), config) + + assert result["phase"] == "reconcile" + assert result["decision_code"] == "PRECONDITION_REFERENCE_UNAVAILABLE" + assert harness.target_store.dispatch_count == 0 + connection.close() + + +def test_same_idempotency_key_with_different_intent_fails_closed( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + + graph.invoke(harness.state(), _config("original")) + original = graph.invoke(Command(resume=True), _config("original")) + assert original["phase"] == "committed" + + changed_ref = "effect/order/v2" + harness.effects[changed_ref] = {"payload": "different-effect", "write": True} + graph.invoke(harness.state(effect_ref=changed_ref), _config("mismatch")) + mismatch = graph.invoke(Command(resume=True), _config("mismatch")) + + assert mismatch["phase"] == "reconcile" + assert mismatch["decision_code"] == "CONFLICT" + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 1 + connection.close() + + +def test_non_boolean_approval_is_rejected_without_side_effect( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("invalid-approval") + + graph.invoke(harness.state(), config) + result = graph.invoke(Command(resume="yes"), config) + + assert result["phase"] == "reconcile" + assert result["decision_code"] == "APPROVAL_RESPONSE_INVALID" + assert harness.target_store.effect_count == 0 + connection.close() + + +def test_denied_approval_is_terminal_without_authorization( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("denied-approval") + + graph.invoke(harness.state(), config) + result = graph.invoke(Command(resume=False), config) + + assert result["phase"] == "reconcile" + assert result["decision_code"] == "APPROVAL_DENIED" + with harness.runtime.store.transaction(write=False) as tx: + assert tx.connection.execute("SELECT COUNT(*) FROM leases").fetchone()[0] == 0 + assert harness.target_store.effect_count == 0 + connection.close() + + +def test_precondition_reference_change_after_approval_fails_closed( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("precondition-change") + + graph.invoke(harness.state(), config) + harness.preconditions[harness.precondition_ref] = TargetPrecondition( + adapter_id="adapter:test", + mechanism="strong-etag", + expected_token="changed-precondition-token", + protected_observations=("policy",), + replay_assurance=ReplayAssurance.DEDUPLICATED, + ) + result = graph.invoke(Command(resume=True), config) + + assert result["phase"] == "reconcile" + assert result["decision_code"] == "PRECONDITION_REFERENCE_CHANGED" + assert harness.target_store.dispatch_count == 0 + connection.close() + + +def test_authoritative_revalidation_failure_is_redacted_and_fails_closed( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + harness.revalidation_failure = True + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("revalidation-failure") + + graph.invoke(harness.state(), config) + result = graph.invoke(Command(resume=True), config) + + assert result["phase"] == "reconcile" + assert result["decision_code"] == "AUTHORITATIVE_REVALIDATION_FAILED" + assert "simulated" not in repr(result) + assert harness.target_store.dispatch_count == 0 + connection.close() + + +def test_indeterminate_recovery_can_be_declined_without_redispatch( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + harness.target_store.lose_response_once = True + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("decline-recovery") + + graph.invoke(harness.state(), config) + graph.invoke(Command(resume=True), config) + declined = graph.invoke(Command(resume=False), config) + + assert declined["phase"] == "reconcile" + assert declined["decision_code"] == "RECOVERY_DECLINED" + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 1 + connection.close() + + +def test_non_boolean_recovery_response_is_rejected_without_redispatch( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + harness.target_store.lose_response_once = True + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("invalid-recovery-response") + + graph.invoke(harness.state(), config) + graph.invoke(Command(resume=True), config) + rejected = graph.invoke(Command(resume="retry"), config) + + assert rejected["phase"] == "reconcile" + assert rejected["decision_code"] == "RECOVERY_RESPONSE_INVALID" + assert harness.target_store.effect_count == 1 + assert harness.target_store.dispatch_count == 1 + connection.close() + + +def test_evidence_provider_failure_is_redacted_before_target_dispatch( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + harness.evidence_failure = True + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + config = _config("evidence-provider-failure") + + graph.invoke(harness.state(), config) + failed = graph.invoke(Command(resume=True), config) + + assert failed["phase"] == "reconcile" + assert failed["decision_code"] == "EVIDENCE_PROVIDER_FAILED" + assert "simulated" not in repr(failed) + assert harness.evidence_calls == 1 + assert harness.target_store.dispatch_count == 0 + connection.close() + + +def test_public_builders_reject_invalid_ttl_references_and_digest( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + saver = SqliteSaver(connection) + + with pytest.raises(ValueError, match="lease_ttl"): + build_stale_safe_graph( + harness.runtime, + checkpointer=saver, + revalidate=harness.revalidate, + resolve_effect=harness.resolve_effect, + resolve_precondition=harness.resolve_precondition, + evidence_provider=harness.evidence, + target_adapter=_DeduplicatingTarget(harness.target_store), + lease_ttl=timedelta(0), + ) + with pytest.raises(ValueError, match="effect_ref"): + make_resume_state( + idempotency_key=harness.idempotency_key, + artifact=harness.artifact, + effect_ref="", + effect=harness.effects[harness.effect_ref], + precondition_ref=harness.precondition_ref, + target_precondition=harness.preconditions[harness.precondition_ref], + target=_TARGET, + operation=_OPERATION, + ) + with pytest.raises(ValueError, match="idempotency_key"): + make_resume_state( + idempotency_key="not-a-digest", + artifact=harness.artifact, + effect_ref=harness.effect_ref, + effect=harness.effects[harness.effect_ref], + precondition_ref=harness.precondition_ref, + target_precondition=harness.preconditions[harness.precondition_ref], + target=_TARGET, + operation=_OPERATION, + ) + connection.close() + + +def test_tampered_checkpoint_fields_fail_closed_before_interrupt_or_effect( + tmp_path: Path, + clock: ManualClock, +) -> None: + harness = _Harness(tmp_path / "truthlease.db", clock) + connection = _connection(tmp_path / "checkpoints.db") + graph = harness.graph(SqliteSaver(connection)) + + malformed_states: list[StaleSafeState] = [] + wrong_schema = harness.state() + wrong_schema["schema_version"] = 2 + malformed_states.append(wrong_schema) + wrong_phase = harness.state() + wrong_phase["phase"] = "approved" + malformed_states.append(wrong_phase) + wrong_revision = harness.state() + wrong_revision["artifact_revision"] = 0 + malformed_states.append(wrong_revision) + empty_reference = harness.state() + empty_reference["effect_ref"] = "" + malformed_states.append(empty_reference) + invalid_digest = harness.state() + invalid_digest["effect_digest"] = "not-a-digest" + malformed_states.append(invalid_digest) + + for index, state in enumerate(malformed_states): + result = graph.invoke(state, _config(f"tampered-{index}")) + assert result["phase"] == "reconcile" + assert result["decision_code"] == "CHECKPOINT_STATE_INVALID" + assert "__interrupt__" not in result + + assert harness.target_store.dispatch_count == 0 + connection.close() diff --git a/tests/test_lease_idempotency.py b/tests/test_lease_idempotency.py new file mode 100644 index 0000000..508882f --- /dev/null +++ b/tests/test_lease_idempotency.py @@ -0,0 +1,283 @@ +from __future__ import annotations + +import sqlite3 +from datetime import timedelta +from pathlib import Path +from threading import Barrier, Lock, Thread + +import pytest +from conftest import ManualClock, SequentialIds + +from truthlease import ( + Artifact, + ArtifactDependency, + AssuranceEvidence, + AssuranceLevel, + DependencyCompleteness, + LeaseStatus, + ReplayAssurance, + SQLiteStore, + TargetPrecondition, + TruthLease, +) +from truthlease.canonical import digest_json +from truthlease.errors import ConflictError + + +def _artifact(runtime: TruthLease) -> Artifact: + runtime.observe(key="policy", version="v1", value={"enabled": True}) + return runtime.publish_artifact( + artifact_id="plan", + kind="plan", + payload={"policy": "v1"}, + dependencies=[ArtifactDependency.observation(runtime.get_observation("policy"))], + completeness=DependencyCompleteness.OBSERVED, + ) + + +def _key(name: str) -> str: + return digest_json({"request": name}, domain="test:lease-idempotency-key") + + +def _precondition(token: str) -> TargetPrecondition: + return TargetPrecondition( + adapter_id="adapter:test", + mechanism="compare-and-swap", + expected_token=token, + protected_observations=("policy",), + replay_assurance=ReplayAssurance.DEDUPLICATED, + ) + + +def test_idempotent_acquire_returns_original_consumed_lease_and_rejects_mismatch( + db_path: Path, clock: ManualClock +) -> None: + runtime = TruthLease(SQLiteStore(db_path), clock=clock, ids=SequentialIds()) + artifact = _artifact(runtime) + effect = {"release": "raw-effect-value-3948"} + + first_without_key = runtime.acquire_lease( + artifact=artifact.ref, + effect=effect, + target="deployment/current", + operation="promote", + ) + second_without_key = runtime.acquire_lease( + artifact=artifact.ref, + effect=effect, + target="deployment/current", + operation="promote", + ) + assert first_without_key.lease_id != second_without_key.lease_id + + idempotency_key = _key("workflow-one") + lease = runtime.acquire_lease( + artifact=artifact.ref, + effect=effect, + target="deployment/current", + operation="promote", + idempotency_key=idempotency_key, + ) + active_retry = runtime.acquire_lease( + artifact=artifact.ref, + effect=effect, + target="deployment/current", + operation="promote", + idempotency_key=idempotency_key, + ) + assert active_retry == lease + + assert runtime.gate_effect(lease_id=lease.lease_id, effect=effect).allowed is True + observation = runtime.get_observation("policy") + runtime.observe( + key="policy", + version="v2", + value={"enabled": False}, + expected_generation=observation.generation, + ) + + consumed_retry = runtime.acquire_lease( + artifact=artifact.ref, + effect=effect, + target="deployment/current", + operation="promote", + idempotency_key=idempotency_key, + ) + assert consumed_retry.lease_id == lease.lease_id + assert consumed_retry.status is LeaseStatus.CONSUMED + assert runtime.get_lease(lease.lease_id) == consumed_retry + + with pytest.raises(ConflictError, match="different lease intent"): + runtime.acquire_lease( + artifact=artifact.ref, + effect={"release": "different"}, + target="deployment/current", + operation="promote", + idempotency_key=idempotency_key, + ) + + connection = sqlite3.connect(db_path) + try: + request_rows = connection.execute( + "SELECT idempotency_key, intent_digest, lease_id FROM lease_requests" + ).fetchall() + issued_events = int( + connection.execute( + "SELECT count(*) FROM event_log " + "WHERE event_type = 'dev.truthlease.lease.issued.v1' AND subject = ?", + (f"lease/{lease.lease_id}",), + ).fetchone()[0] + ) + finally: + connection.close() + assert len(request_rows) == 1 + assert request_rows[0][0] == idempotency_key + assert request_rows[0][1].startswith("sha256:") + assert request_rows[0][2] == lease.lease_id + assert issued_events == 1 + assert b"raw-effect-value-3948" not in db_path.read_bytes() + + +def test_idempotency_key_and_precondition_descriptor_are_digest_bound( + db_path: Path, clock: ManualClock +) -> None: + runtime = TruthLease( + SQLiteStore(db_path), + clock=clock, + ids=SequentialIds(), + trusted_target_adapters={"adapter:test"}, + ) + artifact = _artifact(runtime) + effect = {"write": "raw-fenced-effect-7132"} + precondition = _precondition("raw-target-token-8241") + idempotency_key = _key("fenced-workflow") + + with pytest.raises(ValueError, match="algorithm-prefixed digest"): + runtime.acquire_lease( + artifact=artifact.ref, + effect=effect, + target="target/current", + operation="write", + idempotency_key="raw-workflow-id", + ) + + lease = runtime.acquire_lease( + artifact=artifact.ref, + effect=effect, + target="target/current", + operation="write", + required_assurance=AssuranceLevel.FENCED, + target_precondition=precondition, + idempotency_key=idempotency_key, + ) + assert runtime.get_fenced_attempt_for_lease(lease.lease_id) is None + + with pytest.raises(ConflictError, match="different lease intent"): + runtime.acquire_lease( + artifact=artifact.ref, + effect=effect, + target="target/current", + operation="write", + required_assurance=AssuranceLevel.FENCED, + target_precondition=_precondition("different-target-token"), + idempotency_key=idempotency_key, + ) + with pytest.raises(ConflictError, match="different lease intent"): + runtime.acquire_lease( + artifact=artifact.ref, + effect=effect, + target="target/current", + operation="write", + required_assurance=AssuranceLevel.FENCED, + ttl=timedelta(seconds=30, microseconds=1), + target_precondition=precondition, + idempotency_key=idempotency_key, + ) + + different_boot = TruthLease( + SQLiteStore(db_path), + clock=clock, + boot_epoch="different-process-epoch", + trusted_target_adapters={"adapter:test"}, + ) + with pytest.raises(ConflictError, match="different lease intent"): + different_boot.acquire_lease( + artifact=artifact.ref, + effect=effect, + target="target/current", + operation="write", + required_assurance=AssuranceLevel.FENCED, + target_precondition=precondition, + idempotency_key=idempotency_key, + ) + + evidence = AssuranceEvidence( + level=AssuranceLevel.REVALIDATED, + authority="evidence:test", + evidence_digest="sha256:fresh", + observed_at=clock.now(), + lease_id=lease.lease_id, + closure_digest=lease.closure_digest, + effect_digest=lease.effect_digest, + target=lease.target, + operation=lease.operation, + ) + runtime.trusted_evidence_authorities = frozenset({"evidence:test"}) + request = runtime.prepare_fenced_commit(lease.lease_id, effect, precondition, evidence) + assert runtime.get_fenced_attempt_for_lease(lease.lease_id) == runtime.get_fenced_attempt( + request.attempt_id + ) + + database_bytes = db_path.read_bytes() + assert b"raw-fenced-effect-7132" not in database_bytes + assert b"raw-target-token-8241" not in database_bytes + + +def test_concurrent_idempotent_acquire_has_one_durable_winner( + db_path: Path, clock: ManualClock +) -> None: + creator = TruthLease(SQLiteStore(db_path), clock=clock, ids=SequentialIds()) + artifact = _artifact(creator) + runtimes = [ + TruthLease(SQLiteStore(db_path), clock=clock), + TruthLease(SQLiteStore(db_path), clock=clock), + ] + barrier = Barrier(2) + lock = Lock() + lease_ids: list[str] = [] + errors: list[BaseException] = [] + idempotency_key = _key("concurrent-workflow") + + def acquire(runtime: TruthLease) -> None: + barrier.wait() + try: + lease = runtime.acquire_lease( + artifact=artifact.ref, + effect={"promote": True}, + target="deployment/current", + operation="promote", + idempotency_key=idempotency_key, + ) + with lock: + lease_ids.append(lease.lease_id) + except BaseException as error: + with lock: + errors.append(error) + + threads = [Thread(target=acquire, args=(runtime,)) for runtime in runtimes] + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + assert errors == [] + assert len(lease_ids) == 2 + assert len(set(lease_ids)) == 1 + connection = sqlite3.connect(db_path) + try: + request_count = int(connection.execute("SELECT count(*) FROM lease_requests").fetchone()[0]) + lease_count = int(connection.execute("SELECT count(*) FROM leases").fetchone()[0]) + finally: + connection.close() + assert request_count == 1 + assert lease_count == 1 diff --git a/tests/test_sqlite_v2_migration.py b/tests/test_sqlite_v2_migration.py index eeecb1b..aadbea3 100644 --- a/tests/test_sqlite_v2_migration.py +++ b/tests/test_sqlite_v2_migration.py @@ -17,6 +17,7 @@ ) from truthlease.storage.sqlite import ( _SCHEMA_V1, + _SCHEMA_V2_ADDITIONS, APPLICATION_ID, SCHEMA_VERSION, SQLiteStore, @@ -39,6 +40,7 @@ "fenced_results", "fenced_receipts", ) +_V3_TABLES = ("lease_requests",) def _create_populated_v1(path: Path) -> None: @@ -177,6 +179,37 @@ def _create_populated_v1(path: Path) -> None: connection.close() +def _create_populated_v2(path: Path) -> None: + _create_populated_v1(path) + connection = sqlite3.connect(path) + try: + connection.execute("PRAGMA foreign_keys = ON") + _execute_schema(connection, _SCHEMA_V2_ADDITIONS) + connection.execute("PRAGMA user_version = 2") + connection.execute( + """ + INSERT INTO lease_fence_bindings( + lease_id, adapter_id, mechanism, precondition_digest, scope_digest, + coverage, replay_assurance, protected_observations_json, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + "legacy-fenced-lease", + "adapter:test", + "compare-and-swap", + "sha256:precondition", + "sha256:scope", + "full", + "deduplicated", + json.dumps(["policy"]), + "2026-07-20T11:59:30.000000Z", + ), + ) + connection.commit() + finally: + connection.close() + + def _counts(path: Path, tables: tuple[str, ...]) -> dict[str, int]: connection = sqlite3.connect(path) try: @@ -188,7 +221,7 @@ def _counts(path: Path, tables: tuple[str, ...]) -> dict[str, int]: connection.close() -def test_v1_to_v2_migration_preserves_rows_without_false_fence_backfill( +def test_v1_to_v3_migration_preserves_rows_without_false_backfill( db_path: Path, clock: ManualClock ) -> None: _create_populated_v1(db_path) @@ -198,9 +231,10 @@ def test_v1_to_v2_migration_preserves_rows_without_false_fence_backfill( store.initialize() assert store.integrity_check()["ok"] is True - assert store.integrity_check()["schema_version"] == SCHEMA_VERSION == 2 + assert store.integrity_check()["schema_version"] == SCHEMA_VERSION == 3 assert _counts(db_path, _V1_TABLES) == before assert _counts(db_path, _V2_TABLES) == {table: 0 for table in _V2_TABLES} + assert _counts(db_path, _V3_TABLES) == {table: 0 for table in _V3_TABLES} runtime = TruthLease( store, @@ -239,7 +273,22 @@ def test_v1_to_v2_migration_preserves_rows_without_false_fence_backfill( ) -def test_malformed_v1_shape_is_rejected_before_any_v2_mutation(db_path: Path) -> None: +def test_v2_to_v3_migration_preserves_rows_without_false_request_backfill( + db_path: Path, +) -> None: + _create_populated_v2(db_path) + before = _counts(db_path, _V1_TABLES + _V2_TABLES) + + store = SQLiteStore(db_path) + store.initialize() + + assert store.integrity_check()["ok"] is True + assert store.integrity_check()["schema_version"] == SCHEMA_VERSION == 3 + assert _counts(db_path, _V1_TABLES + _V2_TABLES) == before + assert _counts(db_path, _V3_TABLES) == {"lease_requests": 0} + + +def test_malformed_v1_shape_is_rejected_before_any_migration(db_path: Path) -> None: _create_populated_v1(db_path) connection = sqlite3.connect(db_path) try: @@ -263,3 +312,29 @@ def test_malformed_v1_shape_is_rejected_before_any_v2_mutation(db_path: Path) -> finally: connection.close() assert not (tables & set(_V2_TABLES)) + + +def test_malformed_v2_shape_is_rejected_before_any_v3_mutation(db_path: Path) -> None: + _create_populated_v2(db_path) + connection = sqlite3.connect(db_path) + try: + connection.execute("DROP INDEX fenced_receipts_by_lease") + connection.commit() + finally: + connection.close() + + with pytest.raises(RuntimeError, match="schema shape"): + SQLiteStore(db_path).initialize() + + connection = sqlite3.connect(db_path) + try: + assert int(connection.execute("PRAGMA user_version").fetchone()[0]) == 2 + tables = { + str(row[0]) + for row in connection.execute( + "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'" + ) + } + finally: + connection.close() + assert not (tables & set(_V3_TABLES)) diff --git a/uv.lock b/uv.lock index 6e8ee15..85e6018 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,15 @@ resolution-markers = [ "python_full_version < '3.15'", ] +[[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-types" version = "0.7.0" @@ -15,6 +24,19 @@ 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.2" +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/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + [[package]] name = "ast-serialize" version = "0.6.0" @@ -374,6 +396,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "docutils" version = "0.23" @@ -383,6 +414,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", size = 634701, upload-time = "2026-05-27T17:40:58.442Z" }, ] +[[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 = "hypothesis" version = "6.157.0" @@ -531,6 +599,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -576,6 +665,136 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] +[[package]] +name = "langchain-core" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/05/986c4bb148285791eb59994e0b28947bed96cac7f24467079e4274952a37/langchain_core-1.5.0.tar.gz", hash = "sha256:e1fa09d55b354192c8f60dade06a55bd6add2318c822a684555b8d4a30a16143", size = 967401, upload-time = "2026-07-21T03:37:26.48Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/56/5ef7ba14bac95b0344da18c6e8ec108dce0baf5fc054d1117702f92af29d/langchain_core-1.5.0-py3-none-any.whl", hash = "sha256:f122efee35446632b38687119fca33711abbf3b6b555e31156762298fbe78a65", size = 558510, upload-time = "2026-07-21T03:37:24.423Z" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" }, +] + +[[package]] +name = "langgraph" +version = "1.2.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/4b/0d1130e26b41a99dcc88353bbe7162a1f255c4db746bd94024268e6af27b/langgraph-1.2.9.tar.gz", hash = "sha256:385f87bc1802c35af7e0aa479278ecba8582d103515eb48256cb2ddcd42d0bd4", size = 722869, upload-time = "2026-07-10T01:30:14.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/16/0b8dc48823f1326f3e0c8012a3c07a40da6f194299e2ec080df236287baf/langgraph-1.2.9-py3-none-any.whl", hash = "sha256:c2d98ad94333937922ba04148641c1da2bfe45b5b8e55d7b6dcb0bb2df809e76", size = 247473, upload-time = "2026-07-10T01:30:13.733Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" }, +] + +[[package]] +name = "langgraph-checkpoint-sqlite" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiosqlite" }, + { name = "langgraph-checkpoint" }, + { name = "sqlite-vec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/ea/83917c2369acf8a10a894d4247655fd063c07924ba5bc4e83c85d2eaeded/langgraph_checkpoint_sqlite-3.1.0.tar.gz", hash = "sha256:f926916ebc1b985d802cc9c820026036e84db9d910d62c97b57e4ba64f67d5ae", size = 147902, upload-time = "2026-05-12T03:34:52.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/07/b342811a16327900af2747c752ea19676172fcddf9b592cc384031076623/langgraph_checkpoint_sqlite-3.1.0-py3-none-any.whl", hash = "sha256:cc9b40df0076feae8a9ad42ae713621b148b00ac23adc09dc1dc66090a46e5ad", size = 38587, upload-time = "2026-05-12T03:34:51.231Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, + { name = "orjson" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" }, +] + +[[package]] +name = "langsmith" +version = "0.10.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, + { name = "websockets" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/6d/9ad4427662ef131878f0f928d5a9e9913e0dda4b6511bb03e8722f1dee8a/langsmith-0.10.9.tar.gz", hash = "sha256:195bc67c964a6370cb91742ce9fa07ce69bfae47977f0fb3f41d125b3435d03a", size = 4736750, upload-time = "2026-07-20T21:27:13.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/54/509a1eb6b9e5572a4f3f4b087779d240c6b69f3d5247ffa21314f66155d9/langsmith-0.10.9-py3-none-any.whl", hash = "sha256:5e0e8ab0f8df05710809919184495e33c2a7c9a9a5e8861d63dd12c1226d9c79", size = 673326, upload-time = "2026-07-20T21:27:11.889Z" }, +] + [[package]] name = "librt" version = "0.13.0" @@ -776,6 +995,122 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl", hash = "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", size = 603060, upload-time = "2026-06-22T00:47:00.596Z" }, ] +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" }, + { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, + { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, + { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -977,6 +1312,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] +[[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 = "readme-renderer" version = "45.0" @@ -1215,6 +1605,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "sortedcontainers" version = "2.4.0" @@ -1224,6 +1623,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "sqlite-vec" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171, upload-time = "2026-03-31T08:02:31.717Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434, upload-time = "2026-03-31T08:02:32.712Z" }, + { url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076, upload-time = "2026-03-31T08:02:33.796Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388, upload-time = "2026-03-31T08:02:34.888Z" }, + { url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804, upload-time = "2026-03-31T08:02:36.035Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + [[package]] name = "tomli" version = "2.4.1" @@ -1280,7 +1700,7 @@ wheels = [ [[package]] name = "truthlease" -version = "0.2.0a1" +version = "0.3.0a1" source = { editable = "." } [package.optional-dependencies] @@ -1288,6 +1708,8 @@ dev = [ { name = "check-wheel-contents" }, { name = "hypothesis" }, { name = "jsonschema" }, + { name = "langgraph" }, + { name = "langgraph-checkpoint-sqlite" }, { name = "mypy" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -1295,12 +1717,20 @@ dev = [ { name = "twine" }, { name = "types-jsonschema" }, ] +langgraph = [ + { name = "langgraph" }, + { name = "langgraph-checkpoint-sqlite" }, +] [package.metadata] requires-dist = [ { name = "check-wheel-contents", marker = "extra == 'dev'", specifier = ">=0.6,<0.7" }, { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.130" }, { name = "jsonschema", marker = "extra == 'dev'", specifier = ">=4.23" }, + { name = "langgraph", marker = "extra == 'dev'", specifier = ">=1.2.9,<1.3" }, + { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=1.2.9,<1.3" }, + { name = "langgraph-checkpoint-sqlite", marker = "extra == 'dev'", specifier = ">=3.1,<3.2" }, + { name = "langgraph-checkpoint-sqlite", marker = "extra == 'langgraph'", specifier = ">=3.1,<3.2" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.15" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0" }, @@ -1308,7 +1738,7 @@ requires-dist = [ { name = "twine", marker = "extra == 'dev'", specifier = ">=6.2,<7" }, { name = "types-jsonschema", marker = "extra == 'dev'", specifier = ">=4.23" }, ] -provides-extras = ["dev"] +provides-extras = ["langgraph", "dev"] [[package]] name = "twine" @@ -1372,6 +1802,136 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] +[[package]] +name = "uuid-utils" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/91/63938e0e7e7876658e5e40178e7c0735b53527886fe11797a11699c55edd/uuid_utils-0.17.0.tar.gz", hash = "sha256:abb5667a36119019b3fa320c4d10c21ebccfcc87c8a739e6a0056cee7f48dde2", size = 43220, upload-time = "2026-07-09T13:49:58.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/b2/8f03b61f0aa4afc687855c4f00db35f4d3e58c480cd885abc46f6e41308f/uuid_utils-0.17.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f9b093cb3b6c9d6233ef45a05cab064d2aa0a8cb3c5777084c9e20fcb77c2371", size = 563901, upload-time = "2026-07-09T13:48:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cb/88b909ffb9ac11f88d2e6ceabc592ccc660b5830b06dbcbd290ab8981f1f/uuid_utils-0.17.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0bc4c431ccd59c764080ceb43b126043325fe17861b87759d026a0cdd8423bb2", size = 286383, upload-time = "2026-07-09T13:48:10.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/bc5b64e9898867227c535cd0366c571c580a736748e81329437c1773e442/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00d182e31034250690f417b9068b78eab423c10d76766664e82d9860c340479", size = 323244, upload-time = "2026-07-09T13:48:11.477Z" }, + { url = "https://files.pythonhosted.org/packages/13/d9/8a17462ce066fbf89670fb737a3f0c93a77816736d2a4d134787e759d8ea/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:570db214f6d8507587a8faa968a3fe65e957daeb7bc48b27dc7f69bc3ecdd6f1", size = 330466, upload-time = "2026-07-09T13:48:13.092Z" }, + { url = "https://files.pythonhosted.org/packages/43/37/0c65d0db3bae45183419756d938f1791a82c835fd92bf234eb4f008d2e02/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:351462debd866f1f25e4d4f5c7fac89525b52151f0102a1bdfe94a999b046f5f", size = 443806, upload-time = "2026-07-09T13:48:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/7e698466d1f5254620b5ee0d711fdd20a0e9c2acd7040740c37193a8f673/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:622cdde768300591ac79bfcd7bb3468e4b191b1105d5dbfe8d87c39d8f63dd46", size = 324261, upload-time = "2026-07-09T13:48:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/5d/48/3a5b242d7f0b8e3ca77dcd7177f3cf73e0280cee32e2349d9796ca27f183/uuid_utils-0.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:75d7411e8eb9259764dd60310738540649057cda4509b4af14b36b7f663bfeb0", size = 350657, upload-time = "2026-07-09T13:48:17.273Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/f32ea82a89efed2eafee2f1d925d64687a81e550a9951933fb1b75c95ca6/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1019476b6bdc047216ef7414be5babe0fa5ccfde977c0cac4fd6c75ddec66ff7", size = 500613, upload-time = "2026-07-09T13:48:18.459Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5c/c7b73ec4bbe28db162a4841d352c6eda582801e0dd9fe72f6ad5cc584ee4/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:04452640d8b6920c480c16e5afe91ff896d236e0c972830f9247e0898d38c803", size = 606306, upload-time = "2026-07-09T13:48:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/63/95/8a2777204e8691b4961e6aa619001c3e5175aa430ab43da3079142e8d310/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:793229621e1ad6cac55f015cfa9f4eff102accbc3da25d607b91c6b0bec167fb", size = 567231, upload-time = "2026-07-09T13:48:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6f/1d778ca3ed6d2cf35f22088e2de714675416747ab41be510f22c141043a7/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03815cea572c8a693cab5475b9d750cc161470961c7defa27e9286cad62f38f5", size = 529373, upload-time = "2026-07-09T13:48:22.312Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d3/9ad1ab64b3bed0a0237d1db89dc6f5001d6116a82766753da4ac4496f979/uuid_utils-0.17.0-cp311-cp311-win32.whl", hash = "sha256:c4f845166b09acc65c5213a35551a7f81c17fa010ab467229b5813f79d17fe13", size = 169930, upload-time = "2026-07-09T13:48:23.504Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1a/e01417f52eae6e2cb412260bb332b4ee4b37af2982d9c38cff4b68b2e899/uuid_utils-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:14dc2f46abb1091260c0d203fcbdf4e045042cc07e49183fd3b255904b95eb70", size = 177242, upload-time = "2026-07-09T13:48:24.723Z" }, + { url = "https://files.pythonhosted.org/packages/35/20/396c27f996add19f8ac31e49cc4570824e51a97719087dabf94694d25bc4/uuid_utils-0.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:29179ffb7b317239b6d6afb100d14c439c728770460718280b9c0a42d2561ec2", size = 177023, upload-time = "2026-07-09T13:48:25.834Z" }, + { url = "https://files.pythonhosted.org/packages/20/80/a7e685968e3cec99d6fe2fb25d0f5726310e1bba356da68c13dfd8b7d140/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9205068badf453d2f0821fd5d340389b4679992d7ff79d4f3e5608996dd1b287", size = 556403, upload-time = "2026-07-09T13:48:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/56/47/3102d93bcb7b0bfe6bede63ff8f221a7f91348e10a37f682773be27c56d9/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0fcca4e838af9ac9243b3358d7c14afa4dca286a87781124c272d6c4cad9c968", size = 285608, upload-time = "2026-07-09T13:48:28.769Z" }, + { url = "https://files.pythonhosted.org/packages/55/fb/d59695f0f8db065b93c63316eaafa05a22d75a0486978a33736c52c646d5/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f3729e839209f3457d0d8b6a35a376fdf65577a5aecaf4cc3587d3305759ba6", size = 319926, upload-time = "2026-07-09T13:48:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/5a/03/62fabcd1e990e07a0e220e8d552af45bc16f107fa8e55c2014a706bb1a1e/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dac0ad0cd9a2818d1775215365a4e8c2f8ada215529dd26f3f8cceeb67a6988", size = 327172, upload-time = "2026-07-09T13:48:31.187Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/a5081391338b459e2f8d8b12581f00f8caa6317fab510e0e85c18c59e938/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e671b2322ef09106ecb1ca0f4c398b134d5e2c1f80d7a4f3336847a3072c0e94", size = 439075, upload-time = "2026-07-09T13:48:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/59/30/91795bd01e17a13661280d4899fbf38fb05e3f38e873f9aaec106ec30aa0/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb3e5caca8d3a6f72ea4cce024583f989f6f2e9186f98800213fff0176e8bcc", size = 320247, upload-time = "2026-07-09T13:48:33.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/11/09102b78303e4eb62069d6d88ef9fd661dc523e8f429e1fd67eaa78a6f44/uuid_utils-0.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b72c2002202038666bf647f9a790906214c7c11cd0d6efef77b7d07bef3034a", size = 344738, upload-time = "2026-07-09T13:48:34.786Z" }, + { url = "https://files.pythonhosted.org/packages/74/f9/be95bad6954b60328878c3800258f01a6accd24fd75112d13f023462d53f/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e2ac1c0b56f2c91b6f158e29ed96b1503223fe8aa6e79b1be1dc55bd8a5131c", size = 496845, upload-time = "2026-07-09T13:48:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/02/8a19a34e0530d987488a068a71576a236f5c8c746630b870b57f71eb24ef/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6c142bd0cb4dba31c10babe00d59f7ef6460f0ef55eaa9c1a9da270684af996a", size = 603233, upload-time = "2026-07-09T13:48:37.512Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a8/b1abab36ff73b0248d82179816467f6d39a2e80fd64329a895ca94f3508e/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e252db239eb41c32248e096e0d170bce5896a4fd3405556362bc3dd83d912206", size = 561401, upload-time = "2026-07-09T13:48:38.977Z" }, + { url = "https://files.pythonhosted.org/packages/61/91/70e7b528b351cc03a9ca43e6116371cdde31bb12bcead7ca2ca1367366cc/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:237722b6581bb5b4eb4cefbcbe5c6e2980a440aabe781fbe50ebf1cb71eee4cc", size = 525314, upload-time = "2026-07-09T13:48:40.599Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/9167e90cf9937d6558f92d022ff3024a69d938a514d9c8faa4080f73b001/uuid_utils-0.17.0-cp312-cp312-win32.whl", hash = "sha256:46a73cacdf512f473a81f65dbf84186e08cfe6e9118fa582b6c6b33a8288a30d", size = 166831, upload-time = "2026-07-09T13:48:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7d/0b889654d9ee3413f810cf4685e241285f650d98a4103ac9f3c6bcc95f29/uuid_utils-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:e59b60a0a4cb7541480e02090d37dc2df3b72df4c2e776fff64ce3a4e3dd4637", size = 172944, upload-time = "2026-07-09T13:48:42.992Z" }, + { url = "https://files.pythonhosted.org/packages/be/35/8c6e1bf65e4d400352885dadc656ad6d0af96e89231e3f04686bc2197128/uuid_utils-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:d561a4c5747a1e6c7fa7c49a0292e78b4e8c456332caa084fc7abad8de828652", size = 172459, upload-time = "2026-07-09T13:48:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/d2/dd/614fb9912157ac0128e6050859ccf06d9f13df9a944a803e8f80f6157e38/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d11a7bc1e02da8984d32e6de9e0826c6edac00eac17de270f372bf32f9a0af63", size = 557259, upload-time = "2026-07-09T13:48:45.664Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d072711704de3d21bec08b6c2f36a215200ca1d5e01a390ea1ac434080a0/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7a49f47ac26df3e431c56b825c1bae8e6d3d591fdbb7438c227cc9845a7e3d73", size = 286271, upload-time = "2026-07-09T13:48:47.018Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/8a63e5eb2d5a6ba69a6c2036e305075bd6f5a022e7ea25fc6ce0eb7c51d2/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32df1944808877702ceea398c103881c09a679bb672a215e01c2a84231266bf9", size = 320025, upload-time = "2026-07-09T13:48:48.208Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/bdc2caf9719d9090d7c46043242ae6136cba4f7a7ee384992ab905ad9aa1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98c88d3edd08e7245562e9815996dbc6f0bd4745e1c76462f24af5ae4e187dd1", size = 327931, upload-time = "2026-07-09T13:48:49.673Z" }, + { url = "https://files.pythonhosted.org/packages/b6/33/9219d09d51ead282b578b2a4e0a515c2cce3ec52076cada8bfb7e35727d5/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4370089c8b2e42f1db51d76408c7fa8eaa2934bf854d17983d16179c07c098", size = 438537, upload-time = "2026-07-09T13:48:50.842Z" }, + { url = "https://files.pythonhosted.org/packages/d8/79/e8e0f8b3955f2081c116157119d87659937893242eb834aa170da04d660b/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09a55b7a5ae764985cb46467496a1787678d0a1400356157a080ad95b1a36869", size = 320656, upload-time = "2026-07-09T13:48:52.164Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/d1ceddc430ff04b6e21704b2030d4438074a2f478b265dab43da957791c1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56aa6488b931246fae11924e4bd0e2b32677e63945eecb71c29e3c2ca0dc3131", size = 345310, upload-time = "2026-07-09T13:48:54.076Z" }, + { url = "https://files.pythonhosted.org/packages/d5/62/89438e12f389a843e626b7e37691319a057b3d6b80914609106891faadda/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:309a35f12d99dde19032bc2259cda6431c85eeac0879134dc777cc3087d7e1cb", size = 496771, upload-time = "2026-07-09T13:48:55.365Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/eedcd99f522d60e238ead03844f0d51743ba84d33044959e230b756bf212/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:21c79b61ff750abcf057163dd764ccb6196cde7a26cda1b31b45cd97769e03b3", size = 603631, upload-time = "2026-07-09T13:48:56.746Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a8/bb1b38aaddd7243b6e562c6694f499bf094800918316192fd8cb2cdc2620/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4134353bfe3026ddab8e886002dc52bc5a0ab04611aabb0eaae23c32e6e57f64", size = 562008, upload-time = "2026-07-09T13:48:58.241Z" }, + { url = "https://files.pythonhosted.org/packages/b4/77/5f7ed930dc105e293845c09e4d5bd84076318a12f45a46783e1af64906d7/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7c89359affecebe2e39e6a116d069b363c936511a9572b308402489a26957d89", size = 525527, upload-time = "2026-07-09T13:48:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/fd/25/1b55697adf6811a6f92cff6340e6b03e31fd6bc51066a5c10698c29b3679/uuid_utils-0.17.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:6a019a31bc4db89a0903a3e4f6b218571f3a6ff0ad4b3d3fe1c8f91a05ff6e3e", size = 97965, upload-time = "2026-07-09T13:49:01.217Z" }, + { url = "https://files.pythonhosted.org/packages/26/bf/cd729343de4684230be8a966bad7bfc2cf10ce3e643b1189a8b5370dbe35/uuid_utils-0.17.0-cp313-cp313-win32.whl", hash = "sha256:b3131a82d0c7611f0aa480a6d36929e001a3f54ba0fc029a8118a5863cce513c", size = 167316, upload-time = "2026-07-09T13:49:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/76/f0/e602ae0a1b139a7826e5189b93d91902564def06d5006324fd2faf82c8fc/uuid_utils-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e311f908d2f842fca4c7dcebc4f10306b8089b204ef04cf6704b4332c9ff6ff", size = 173630, upload-time = "2026-07-09T13:49:03.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/024ebece265b387154115dc4f1d9727174ef82623069f4bec8b7ed7e73f7/uuid_utils-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:c351737e2e65497c7200ab4ffb8af97e9f48be6488309abdd265fe08d66ee92f", size = 173214, upload-time = "2026-07-09T13:49:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/56/44/e2fd3fdf356e1b55d2acf1b956b4f3f29ffb215a99c387eba04b1c5fba66/uuid_utils-0.17.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:673d89cc434cc9b97a0b4cf61272f6fca70a81f64eb0afbface2a0d9f77f06cd", size = 562232, upload-time = "2026-07-09T13:49:06.201Z" }, + { url = "https://files.pythonhosted.org/packages/19/28/65e0980d668a6d44e699f59d1acf43d6b5d4893592c115ce7c680bb4dfa1/uuid_utils-0.17.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:387cf7437c94ddec08651a0f1081381299c7075bc48a6251d8922bf39973378a", size = 287858, upload-time = "2026-07-09T13:49:07.45Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8d/5e97bcebc90fb6a10f98af3dc1ba552e04183aba59e2edc0b9cf486dd998/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:220b52746d99e11964badac3c0869016e0c24bafb70a7dd5c2c072a6be3da9cc", size = 321587, upload-time = "2026-07-09T13:49:09.489Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d7/88b2a2370cc3d455ba0515fb6f5c8f7ac0c0f55a86801b6e56a432f22c17/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ab4a66e7a035ad6625cfc1fbdb34f5c2d25a80ae1ef4bfee458ea2036333c6d", size = 328964, upload-time = "2026-07-09T13:49:11.292Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/181c5da673953dfc0958cb4fb3a4984a9098673ddb05cac68e994bc8511b/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5641071337eb11d61a001ea08793bf72216f3241f0a433ed2764804b2a3e3cc7", size = 442909, upload-time = "2026-07-09T13:49:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/ec/38/5c5e665af542884a8fd3c61725c38453239e13940326b5b70f3ef8881a97/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9082e709014946b1f6e96ae6ecd93652efca2d2a6a3ab67dbe151c8b4bf193a4", size = 323076, upload-time = "2026-07-09T13:49:13.897Z" }, + { url = "https://files.pythonhosted.org/packages/f5/35/7de97de18cbf226c2a4f2104ad15e56ca4491717c81c0b71795c0c585b4e/uuid_utils-0.17.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1fd6f0e8a162dc0e9255b6aebe3cd175e76c33202f1bf39da9e6294b93db0099", size = 347360, upload-time = "2026-07-09T13:49:15.237Z" }, + { url = "https://files.pythonhosted.org/packages/26/a1/9915d5dd59fdd1957ded5d188c0ea0b9db5a1d84d42c8d8828a7b83b366e/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d63010803d7c368963bbe6f7ec379593e76dd581d7db0f29118d88713c9e0354", size = 499267, upload-time = "2026-07-09T13:49:16.774Z" }, + { url = "https://files.pythonhosted.org/packages/c0/05/88108405262ec850cea0f95733445d6873e5772af3292baabd9ef8457740/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a46bedc273b6f58f11dee816ff74999625ef8d007890f411b7a4975bf1c89330", size = 604940, upload-time = "2026-07-09T13:49:18.147Z" }, + { url = "https://files.pythonhosted.org/packages/89/d5/6dbcd300de47cc443cff2656cd5327a385751213dcb2101cfee7388170b2/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:405233a5f625b3d995648f4647fa6befa4567cf3f74e1f6b9837e16f7310f0e0", size = 564172, upload-time = "2026-07-09T13:49:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ab/94/e8057f2288a415fba8a978bca4b589f5cb6b91a028a5dc07a1775938b33f/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6c5d2d71e1f17329150ad9427d27f4a3f29a01792e7ecdc64a98ac5368fc4d5", size = 528533, upload-time = "2026-07-09T13:49:21.075Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6b/31713148c77e48e62f51aa042a98a54a8be0396912ea5130f83f52ae722d/uuid_utils-0.17.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f7e9b8728ba07a3cb2f29d5aa1a266c2664eb8ef0fd43afa34627c92f7fac8f0", size = 99197, upload-time = "2026-07-09T13:49:22.351Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f3/ca6f6ac5428312df8ed632f6dd9f9e6aba23090471fcdeae53eab027e8b3/uuid_utils-0.17.0-cp314-cp314-win32.whl", hash = "sha256:58838921e377791ef22c64cc92141bfae030f43651ff9272f0f28a208a9e6a5a", size = 169540, upload-time = "2026-07-09T13:49:23.563Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cd/7ede0db66411fa09817d79b680f7454ea9bee2d374e1922e4efd065760a3/uuid_utils-0.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:42275ebd0e8e74e32cdbfb8bd88fc99576567d51d54a508020611fd8f4f463a0", size = 175984, upload-time = "2026-07-09T13:49:24.703Z" }, + { url = "https://files.pythonhosted.org/packages/f0/81/533b5f80cd4918c0693f4e1b7b90ceb1caa45f4266ae8b528135d7ecca5d/uuid_utils-0.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:b5d11cccba076a32321ef1380dea956821f0b51794ef59df64e58fb1cd543aae", size = 174749, upload-time = "2026-07-09T13:49:25.886Z" }, + { url = "https://files.pythonhosted.org/packages/a0/13/f400ac39d06fd8be5b099c09e41bb975205926722a3e8d53348817cb7ff9/uuid_utils-0.17.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fae8b282f0cb22a5de222999f7723f4e5ec04f6fcdf4aaef879b5b36625ae2b0", size = 562610, upload-time = "2026-07-09T13:49:27.374Z" }, + { url = "https://files.pythonhosted.org/packages/03/8c/c71c8312304c56f6d0bcba87cd402fa79bec35d18ffc8c41954196ca68e5/uuid_utils-0.17.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:967955620df45e6cffe2e9950cb9903cb455649396f896b26b04363a91a5054b", size = 289473, upload-time = "2026-07-09T13:49:28.989Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cd/522117e2e5184ca1d4f0f85ee833e9e21bd8c6b99eff8a4d1a8e5a194e33/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375cde148430d60a4a07c03abaa0774c4fddfdd90de99b4ba02f24088bc9d750", size = 321600, upload-time = "2026-07-09T13:49:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f4/0d81f9bd346fc717bc561c08fa6457e0328966eb76e536b938fe77d56459/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:975c17da26c5b9d46c336b03c52a057ac28378d6f9d98b58d32a038589bb3912", size = 329569, upload-time = "2026-07-09T13:49:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/5e/41/26e1363f36a94c9e8ec2dd21d5f63088d3e7c723adbb12dcc8fdc77be417/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3150d836290c88f1d26eb59c4db280d87417dd3bfaadd2889c77416c8f0ff6fa", size = 442051, upload-time = "2026-07-09T13:49:33.024Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a7/2c1ed1b34d7df7fdcc11c28fd26d94d44843b37d9af2435ff9fd8abdbc08/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9472a8de37faf8bd216c628e0e68c8f6bef730d3ba0a5060f3b0fa460c992ac2", size = 324372, upload-time = "2026-07-09T13:49:34.554Z" }, + { url = "https://files.pythonhosted.org/packages/78/bf/328d3c6bb22c496944a1b3b732207d71aa6964eb604e5e3b9dcb91ed0a00/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d27c531edb8d1f38ca2eddaa1fa24913a460aeb721f2efd4ef42a124ce94e354", size = 348548, upload-time = "2026-07-09T13:49:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/76/a07de5cb7b90582fdbbc830fd19be129cbbb9897cfe239fef469d7bd2d09/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5670c52a438e21483ce715776144914a4e2a2a5c62d9dee15f8a3e90cf128ae6", size = 498985, upload-time = "2026-07-09T13:49:37.142Z" }, + { url = "https://files.pythonhosted.org/packages/f4/62/9966e46ae34fcec6b06119631fb3c09705ea78835035ce3a82d3348eb61a/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:6f29689a76fe7a49cbd629a794d0ec1eab48814e323a00a146a741b0195bde68", size = 605183, upload-time = "2026-07-09T13:49:38.648Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4e/bb962ba0fe31e903b199f22cf4c1a6cba35a8987aef526d287277ab8ca8b/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4441600447d340ae103a353f01dbcd22ff680e5ee1a22988efe8d7b791d8fdb3", size = 565412, upload-time = "2026-07-09T13:49:40.115Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/122adfeeeae8a84ccfd43bce627b104d12a2180a93bffd2c0e1b54dad7a6/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b04935a79c03c41ad08d0a5f390aac968bfb561f1268897bc5b0f077971efd", size = 529885, upload-time = "2026-07-09T13:49:41.513Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4f/257304dded339dc35fc9bf35722ac68fd4fdb930f255b8f7bccdf74ebba9/uuid_utils-0.17.0-cp314-cp314t-win32.whl", hash = "sha256:239d8a281fe10bae33205b5d43185834d556b18434e0a113b5dc1dfb2fd97e91", size = 169472, upload-time = "2026-07-09T13:49:42.871Z" }, + { url = "https://files.pythonhosted.org/packages/35/c8/e78c06db7e9ce317ce7b8759ff2058333eac75caa8c22b75f0059589c9be/uuid_utils-0.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e288a06cbbbcd01b44386e767985c9e21d2ad9bf59829aa7058d9a2a494804ab", size = 176271, upload-time = "2026-07-09T13:49:44.105Z" }, + { url = "https://files.pythonhosted.org/packages/a7/11/bd1c70e1ad3301163cebe66c8d26de26e6814d52f642a849448bd2833626/uuid_utils-0.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1776a80d16369999b21627028cc5dbce819be83e1e079fdd7a51b587d2916db9", size = 175004, upload-time = "2026-07-09T13:49:45.591Z" }, + { url = "https://files.pythonhosted.org/packages/ee/14/4ae708968b15cac7b68d5b854bfce724b21faa1c7a5147fb96d87f468a45/uuid_utils-0.17.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7b9044ce4acbf392d4b3a503fe377641f4deff82e6c341c36ef27af0dea76cdf", size = 567823, upload-time = "2026-07-09T13:49:46.902Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e2/d3af9c3d1dc6efb9ee1cffab30f3f2aacacc3892b21b495d78d34c6696bc/uuid_utils-0.17.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a91c4814c7150a4d798da691b7804eacd78c4b84fb392a60fa0de21341861eb", size = 288763, upload-time = "2026-07-09T13:49:48.491Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c2/f1b183e412387529893015a94a8447633c665f6d0392de20e245680e636a/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd4a21baaac9a88486f0dd166c5793feb101a0bb9f006f2c401657fff5a1343", size = 324919, upload-time = "2026-07-09T13:49:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3c/d32c799bdd51f3b08b6ee95f9de921b59c69075a96767f937fab55014813/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32abaafc8e91928b3d9f4d82e42d2094041e38ad6bb964066faadff28e4162f1", size = 332689, upload-time = "2026-07-09T13:49:51.402Z" }, + { url = "https://files.pythonhosted.org/packages/6f/90/b4cd455619ff276dc3c3262a7420ead63aa1e531362f00df4cdb07d90e0a/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd741c73440b328f937dc53b344ecadc46bc4f0cec0333a8f42b55f3468ce7ec", size = 445726, upload-time = "2026-07-09T13:49:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f1/5cc042a37932aa9a66eb8ab4a9a5b31d80261ae4565ff0193d8cc1fb9392/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89a0980d49683c00539c59cd9f46b1908c538e6b5b0a48ad12187bb856d0f391", size = 325610, upload-time = "2026-07-09T13:49:54.191Z" }, + { url = "https://files.pythonhosted.org/packages/5e/72/9e800c41d766484484e97845a7a7f677ba94462df86c97183e0290229d16/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:de1064663aa7c839286488a319d2b3b478ca5ab5b2091ade888ed0eeca11a98a", size = 352672, upload-time = "2026-07-09T13:49:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8e/86ce2c03a1d9674530f6649e49067f7c69929600127077731de590d12132/uuid_utils-0.17.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2db386941cfdecdd0b5a8ceeed5cf7479c83d1730dcf64a48d43cfa018cc3310", size = 178681, upload-time = "2026-07-09T13:49:57.096Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + [[package]] name = "wheel-filename" version = "1.4.2" @@ -1381,6 +1941,144 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/0f/6e97a3bc38cdde32e3ec49f8c0903fe3559ec9ec9db181782f0bb4417717/wheel_filename-1.4.2-py3-none-any.whl", hash = "sha256:3fa599046443d4ca830d06e3d180cd0a675d5871af0a68daa5623318bb4d17e3", size = 6195, upload-time = "2024-12-01T13:03:00.536Z" }, ] +[[package]] +name = "xxhash" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/63/71aa56b151a1b28770037a61bd4e461c2619cfc8866a4fcaf1548605e325/xxhash-3.8.1.tar.gz", hash = "sha256:b0de4bf3aa66363552d52c6a89003c479911f12098cd48a53d44a0f7a25f7c46", size = 86223, upload-time = "2026-07-06T10:49:58.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/5a/05eaa129555f85476a3e16ff869e95f81a78bbe4647eef9d0229f515a317/xxhash-3.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602efcad4a42c184e81d43a2b7e6e4f524d619878f2b6ee2ba469011f47c8147", size = 34699, upload-time = "2026-07-06T10:44:10.14Z" }, + { url = "https://files.pythonhosted.org/packages/80/59/0df1133958b2228929355e022aab1e958c7b2c43e27bf7f59bc9edfa8a54/xxhash-3.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:131324f719957b988861714de7d6ddf57b47abec3b0cc691302ffeaba0e05e10", size = 32373, upload-time = "2026-07-06T10:44:11.353Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bf/1cfda5b5e6bf26617812b4a31662ef2220d2ad04e0a55b8ff9eb36e56a5c/xxhash-3.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db77278a6eddadbf44ce5aae2fee5ebb4d061f026b1ce2130d058cd4d7a7b670", size = 220284, upload-time = "2026-07-06T10:44:12.683Z" }, + { url = "https://files.pythonhosted.org/packages/70/93/45dc0ad7913b69e5b08bd039236cf628380e4c9cc76a8a4c6625a328e058/xxhash-3.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c332dd48b8cb050da2bb2a3c96d72b1664168650a250ef9718e423df7989e05", size = 240980, upload-time = "2026-07-06T10:44:14.297Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/f28ba7d17f2c1410ee397982c817ab1bd5b2701070c2d2c373539aad000a/xxhash-3.8.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a5cd96f6dcdf4fa657b2d95668d71d58455248f98712ecffaa9c528edf40ccae", size = 264526, upload-time = "2026-07-06T10:44:16.017Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/f10651cec2c7981b20d693deae6bdfc438427d92be2db4ccabb6181f0021/xxhash-3.8.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c959f88160b13b4e730b0d75b459b7929fc0d2225c284c9683ac95d6feeeac6a", size = 241369, upload-time = "2026-07-06T10:44:17.698Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/136e0cbaf5db51e191423b1c98643593189f02b6cd90837bf64b19113d70/xxhash-3.8.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:027dee4355f3fcc41481650d846cf6cfc895c85a1ab7acd063063821a0df5b4c", size = 473186, upload-time = "2026-07-06T10:44:19.354Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3f/6aa808a96bdc43dba9a740dec56c744526ee3c0019e32c75e810fa90ae4d/xxhash-3.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad52a0e4bcc0ba956a953a169d1feec2734a64981d689e4fc8f490f7bf91af60", size = 220092, upload-time = "2026-07-06T10:44:20.956Z" }, + { url = "https://files.pythonhosted.org/packages/47/28/a8675e78a9ced96dab853416162268e10e05b452e95db7888cf69f58ac5f/xxhash-3.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d3dfb1f0ff146da7952867a9414f0c7a29762f8825a84879592612fd6139342", size = 309846, upload-time = "2026-07-06T10:44:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/0f/7fe4d4ef4e69f0033e012396ee2a115886bca7b10b7e45ce398626436bfc/xxhash-3.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4482380b462ca9e59994d072a877ecadd1cf51102daeeab2db696f96ab763723", size = 237659, upload-time = "2026-07-06T10:44:24.135Z" }, + { url = "https://files.pythonhosted.org/packages/38/8f/83e9e31d4ed57fe963b99cb5b13a23e3e0f0dad1885aa0ebd2a7819dd423/xxhash-3.8.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:950ac754d16daea42038f38e7465eb84cda4d08d7343c1c915771b29470f065a", size = 268737, upload-time = "2026-07-06T10:44:25.875Z" }, + { url = "https://files.pythonhosted.org/packages/57/79/7e7de46dbe5d1f49afc96a0bc42e6b8df24eae3d6bad6007b99e42f48430/xxhash-3.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0418ec8b2331b9d4d575fc9284427e8e69449d7172e99e1a86fcdd1f51a0a937", size = 224955, upload-time = "2026-07-06T10:44:27.777Z" }, + { url = "https://files.pythonhosted.org/packages/ec/34/b8540839e958d5ef5c6101af6f16032109e7099698ae8edbc8dcefe4d8f4/xxhash-3.8.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:32a94ad2763e0263d9102037d349002c3d3c401e42770542c3eeb4801f311661", size = 239653, upload-time = "2026-07-06T10:44:29.422Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/a735d05f7f859354acadabe470ff40e2c46672275f96dcf096a761904def/xxhash-3.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:89b11a5cdd441aa463f6d34ca0241602bc09b001a76994b6059828494108c673", size = 300213, upload-time = "2026-07-06T10:44:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/3e1cb020237b68117fc212dc5f9753b87f865b4dfee7c1ce62d0836955b5/xxhash-3.8.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:09a204dd4bb0823daf938cdd0dc8057d5f1e14fe3cbde929424255f23f9de872", size = 442508, upload-time = "2026-07-06T10:44:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/f80090622141cc734b039ce1d15ce3ff6dced375e9680249bf5b9b8c6bf9/xxhash-3.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e710ad822c493fb80a4fbc1e3d0a807b1422cb90adbe64378f98291b7fa48fef", size = 216853, upload-time = "2026-07-06T10:44:34.983Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/60157acecc307b238d3651c2483168e224b48b23a36ae6d6903588341d80/xxhash-3.8.1-cp311-cp311-win32.whl", hash = "sha256:5013be3bea7612852c62a7437f3302c1cfb91ca7e703b194459db0b2b2e0d792", size = 31936, upload-time = "2026-07-06T10:44:36.542Z" }, + { url = "https://files.pythonhosted.org/packages/59/5c/ef70c418d878d187b8da56d4cdc06aea6cf5e456b301e96e51e1d2cc8625/xxhash-3.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:f377012b86c0a23a1df0cf5a1b05aa7187649e472f71c7892e5f2c2815bbe74f", size = 32724, upload-time = "2026-07-06T10:44:38.177Z" }, + { url = "https://files.pythonhosted.org/packages/2c/25/f008db952cec6b2a26445b456eeed2ebebd65e08e848ebe09ed6ac0634e6/xxhash-3.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:836f11d4474d3228e9909d97216faa4f7505df41cfaf3927eb29809de785a78d", size = 29212, upload-time = "2026-07-06T10:44:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/42/91/f65c34a7aa7b4e7cf4854f8e6ef3f7ee32ceac41d4f008da0780db0612f6/xxhash-3.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e6e49370822c1f4d8d90e678b06dbcb08b51a026a7c4b55479e7d467f2e813bc", size = 34680, upload-time = "2026-07-06T10:44:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/57/04/b10a245a4c09a9cfa88f8e9ae755029413ad1ac17047f9a61906e5ae0799/xxhash-3.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:220d68130f83f7cc86d6edfdeab176adc73d7200bf3a8ec10c629e8cf605c215", size = 32397, upload-time = "2026-07-06T10:44:42.196Z" }, + { url = "https://files.pythonhosted.org/packages/3a/75/45ab795b5945b6388583bd75202106af505537935566c15a1577797a0e08/xxhash-3.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d365ee1892c1fa803536f8c6ce21d24b29c9718ec75eb856095c07830f8c478", size = 220549, upload-time = "2026-07-06T10:44:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/13/44/5ba2bd0a14ddf4193fc7d8ec29625f659f22c06d60b28f04bf46305d8330/xxhash-3.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:852bfe059720632e2f16a6a4745e41d20937b2bf2a42a401e2412046bb6971cc", size = 241186, upload-time = "2026-07-06T10:44:45.534Z" }, + { url = "https://files.pythonhosted.org/packages/23/32/c4147def4d1e4538b906f82731e0ba23424377fc50a7cddd03cd284c8f63/xxhash-3.8.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2f8c25a7061d952de589bd0ea0eaadee32378ff83dd6a677b267f9cd86f401f8", size = 264852, upload-time = "2026-07-06T10:44:47.199Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bd/71ed14f4f0318bb7fd7b2ec51999413487fa8da8d41208e84d50d1ef0f98/xxhash-3.8.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:868a8dcaff1a84ba78038e1cef14fc88ccf84d9b4d12ea604696e0693296aa56", size = 242663, upload-time = "2026-07-06T10:44:48.846Z" }, + { url = "https://files.pythonhosted.org/packages/91/09/70af22c565a8473b3f2ae73f88e7721af281bc4a575236dbd1970c9f76f6/xxhash-3.8.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6536d8677d2fff7e64cd0b98b976df9de7aee0e69590044c2af5f51b76b7a170", size = 473510, upload-time = "2026-07-06T10:44:50.695Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/34db781c8f0cf99c544ca1f2bc2e5bf55426e1eb4ca6de8ea5da56a9f352/xxhash-3.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82c0cedd280eab2e8291270e6c04894dbc096f8159a39dcf1807429f026ca3cc", size = 220469, upload-time = "2026-07-06T10:44:52.422Z" }, + { url = "https://files.pythonhosted.org/packages/93/5f/9a184f615fa5a4dce30c01534f62946ce5a11ce40f73785cbd356ccabaa9/xxhash-3.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daa86e4b68221d38e669bb236ba112d0335353829fb627c82e5909e4bbe8694c", size = 310290, upload-time = "2026-07-06T10:44:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/a9/dc/9b9a9789011ee153723a5eb9e7dd7fcbae2ba9b3fe7a729249ca7c252056/xxhash-3.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2bc7113e6f2b6b3922dd61796ca9f36af09da3773898e7003038dc992fc83b8d", size = 238173, upload-time = "2026-07-06T10:44:55.693Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4d/71c6005ada9dcb608a4e1902e8475ecadb5f3fbfa04e1e244d276a2d0c43/xxhash-3.8.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5eed32dad81d6ba8e62dc7b9ffa0500199385d7810a8dd9d4eafaceb8c6e20bb", size = 269026, upload-time = "2026-07-06T10:44:57.424Z" }, + { url = "https://files.pythonhosted.org/packages/2f/87/d6c036ba25dfbd9c8633be5aa86fc9474bbb9e2c68212a841d090abe7344/xxhash-3.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:83697b0ea1f10e7f5d8b26a4906fa851393c61546c63839643a2b7fe2d868061", size = 224970, upload-time = "2026-07-06T10:44:59.085Z" }, + { url = "https://files.pythonhosted.org/packages/48/62/4c1f035a41c5752aa05e195b6c904c07b94fe9061a16de61e72a6e6b135f/xxhash-3.8.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:36fc69160465ae75c6ec4ac9f781bb2aa16ae7ff869e73c26fee85fbb11b9887", size = 240820, upload-time = "2026-07-06T10:45:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/d39d565069b87e86d21a2af2a31d04db79249d25aa8d5b62959056a89857/xxhash-3.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:445e0f5a31f2f3546ae0895d4811e159518cdc9d824c11419898d40cfadb677e", size = 300619, upload-time = "2026-07-06T10:45:02.716Z" }, + { url = "https://files.pythonhosted.org/packages/13/22/75467acc887edc8cf71c97ab1708feb3df7a88bda589b9f399765c6387d2/xxhash-3.8.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:dfe0580fbfd5e4af87d0cc52d2044f155d55ebd8c8a93568758a2ea7d8e15975", size = 443267, upload-time = "2026-07-06T10:45:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b6/1da3baa5fa6ef705e3425fddd382be7dfc4dfba2686df90a20f16e9c7b1b/xxhash-3.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:095e1323fa108be1292c54c86da3ef3c7a7dc015b105a52133973bc07a6ad11a", size = 217338, upload-time = "2026-07-06T10:45:06.304Z" }, + { url = "https://files.pythonhosted.org/packages/78/dd/b5295a9f97484e7a1c2b283a742ca45e3104991c55a1ef670dde161829ba/xxhash-3.8.1-cp312-cp312-win32.whl", hash = "sha256:bf28f55e427e0483acb1f666bd0d869b6d5e5a716680c216ad7befe3d4cfba2e", size = 31970, upload-time = "2026-07-06T10:45:07.823Z" }, + { url = "https://files.pythonhosted.org/packages/ec/31/3fa0b807d7e21515cd975e7fe5c039d52ac3e9401a96d6ad68dae6305215/xxhash-3.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:2256e80e4960ee282f63428adb349cb7f8bd8efe4db770d88eb815f4b9860724", size = 32741, upload-time = "2026-07-06T10:45:09.42Z" }, + { url = "https://files.pythonhosted.org/packages/b8/05/86feada74e239600e6875aa507afb40482a89b92700aa74a92da83bdcb77/xxhash-3.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:9df56e6df96a60590935e22373041cccc91fd55858763dcffb55bf63b3a2b396", size = 29234, upload-time = "2026-07-06T10:45:10.809Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8c/446bb782cd0d27007a917b5569a08dd73219c3e8d6e459014db104b27bdb/xxhash-3.8.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:3c682fcd96eb4bf64be32a4d95f96107e1588005831bd8a741b324fdda01b913", size = 38562, upload-time = "2026-07-06T10:45:12.425Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/c0c45627eaa6be7a5d6117423adf8f7a15b17ee74b4b17072cca5959a225/xxhash-3.8.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:036a024d8b9c01f70782e09ed98d532e76fd23f950ae7154bd950fe94e90ebec", size = 36656, upload-time = "2026-07-06T10:45:13.932Z" }, + { url = "https://files.pythonhosted.org/packages/f6/94/8324c04cc7597154caaeba6c094e01fbd2e7601d01e7a13eea9f5420e77b/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d6a5c0bce213b23b0166fe0d35bcbbe23ce4b968f257cc7eb6fd57cb8e1e6297", size = 31169, upload-time = "2026-07-06T10:45:15.687Z" }, + { url = "https://files.pythonhosted.org/packages/40/a4/beb6bb26e1184e126dbe7a5682330214ef54dcfbf882078aa9f4b5428d42/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5177aa44eddaa97c6ef0cc00c6d540edb64d51781d2f8fb941612ec61a92c9ed", size = 32177, upload-time = "2026-07-06T10:45:17.035Z" }, + { url = "https://files.pythonhosted.org/packages/56/0f/fc4c92a5a528f839b34b6419b2e53c8597f2a629d5a1f5d721f65bfa1fd6/xxhash-3.8.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7801b7223db017b9c0c9ccf37e44524edb35a1544a1c032add22c061c6af0276", size = 34642, upload-time = "2026-07-06T10:45:18.39Z" }, + { url = "https://files.pythonhosted.org/packages/d4/58/edbfb141d4000767ac6a9694f8ac0763e2c2e983e65c9e31620ba56e2667/xxhash-3.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e80238259655bf69d7bcd08226a970d7f42605f3157786bfa76dd13472d7fa0", size = 34684, upload-time = "2026-07-06T10:45:20.033Z" }, + { url = "https://files.pythonhosted.org/packages/07/3f/5072f1f0f5714186f0ac2a0b5a4929ce30d4b845e94886b6c01b6ebda0be/xxhash-3.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bcab50a389cc04d87f90092af78a6adba2ab3deca63175a3344ca83514045315", size = 32401, upload-time = "2026-07-06T10:45:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/49/c7/802ea2f9c2ed59219934d6d65c470d502b1788043eae277a52af8658bda6/xxhash-3.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2489d3a776fa380cb8e71f54c7fda268a9baf3de9b1395093fd280f95735907", size = 220617, upload-time = "2026-07-06T10:45:23.234Z" }, + { url = "https://files.pythonhosted.org/packages/99/a8/e10488efd31fcb13fcd6acbc6e788f10c6f8e3a0cc4ae3eb89dc19c55a12/xxhash-3.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ab1e5432690276e71192be7401b55f96db2d0eedea5d44eb1f164505669cc0", size = 241295, upload-time = "2026-07-06T10:45:25.364Z" }, + { url = "https://files.pythonhosted.org/packages/18/cc/14180b17d44892a631f8ae7323c30bfbb1328efc8209e528a480293528ac/xxhash-3.8.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b30e01a0b97a4bc3f519a4d7a82da3dc53251fb0de5eeea8660dcd4ff094c0c2", size = 264688, upload-time = "2026-07-06T10:45:27.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/72/a14019d0c5f6c41ee407a503036ae32787c91325ca218a96a9b5627be651/xxhash-3.8.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f44275ddb0978b67a58a951501903f04d49335a91f7681c9ce122ecb8ccb329", size = 242740, upload-time = "2026-07-06T10:45:28.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/92550e556c6fcfcb96c6a336945eb53a431ed43120ed749636debb16c5cf/xxhash-3.8.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3b87cbd974512c0c5fc7b469c36b2cdc9ee6d76e4ec78bccb2c7184611c49b0", size = 473599, upload-time = "2026-07-06T10:45:30.524Z" }, + { url = "https://files.pythonhosted.org/packages/29/83/e361d3c1acd1b21e1d489616de6fa4aaf843365d8179f612e3743eac20a9/xxhash-3.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98ee81b4b7f3023c9cb04a78cc67610baffcb5812d92f2096cb5a5efc6f19437", size = 220559, upload-time = "2026-07-06T10:45:32.979Z" }, + { url = "https://files.pythonhosted.org/packages/05/01/006a4243c2c2a6831827f9999f6d1c23feeef100eb023c1f886022a00bf3/xxhash-3.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2666f059a1588a99267e33605365ed89cea92f424b3522806a9f4bd8ad2e3d62", size = 310383, upload-time = "2026-07-06T10:45:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/d8/20/af388e8bf9f9a0f89eeef7d2a1935d176ee1c20bc6adeda05035879379cf/xxhash-3.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0093cf7eeb91b84776e8742113afa4bdf47533d36cf719179aaaf1f56f6f8bf", size = 238228, upload-time = "2026-07-06T10:45:38.02Z" }, + { url = "https://files.pythonhosted.org/packages/63/6b/4666579a87eebd1744663c404297355fa0658617b015cedfa58810ee7036/xxhash-3.8.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3a800912a2e5e975d4128969d645c4a2a80aa886ccd6c9b1c6f44529e327e8cf", size = 269137, upload-time = "2026-07-06T10:45:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/de/d3/e963a8a46f900a137d91b02144d8ea07a8f812971b138204a3b2f8b8e55c/xxhash-3.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0fe37f72a207223d22a4eddc3149d4298993385aa9daef25c039246ca5a309f3", size = 225068, upload-time = "2026-07-06T10:45:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/aa/80/9d181dbcde4b0fe48375f48833a5832d4b8cd2b349b15110c92ee472d874/xxhash-3.8.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5db43f249b4be9f99ef4b967863f37094fb40e67effafb78ba4f0356b6396104", size = 240874, upload-time = "2026-07-06T10:45:43.414Z" }, + { url = "https://files.pythonhosted.org/packages/39/15/ce3ab5a1cd27ead25a5196e55a7284220f6ad6e316da494ffd900b2b600f/xxhash-3.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c4ed42965c2cd9081f011be22f69d0e65d3b6165fe7734072fd0c232840bbd4e", size = 300702, upload-time = "2026-07-06T10:45:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/96/c0/2281a8ab5f2a62dbf57a23c58a01ccc1d98abf40f71193c8a81f59e759b5/xxhash-3.8.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3557bec8fcb11738a8920eeb68974bc76b75262f6947998d3147954ce0a4b893", size = 443351, upload-time = "2026-07-06T10:45:47.188Z" }, + { url = "https://files.pythonhosted.org/packages/81/2e/071a58c1a53a52d4f7a3aa0987be0c396dffd40da8204805fe1b130a81f4/xxhash-3.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00de40f3b42240db23a82a5c682b55d7263d84a26a953240c1aee463409660e3", size = 217396, upload-time = "2026-07-06T10:45:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/68/44/36ab58134badd9d3433fc7b53c4ca8d113d8e807782885628640f8297a4d/xxhash-3.8.1-cp313-cp313-win32.whl", hash = "sha256:b5196cc2574cfec572a5f3fb7cfa5ade27305ae3d06516a082132441aff4c83a", size = 31974, upload-time = "2026-07-06T10:45:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/2a0b84798448e766f7b89ceed073cb0cb5a43fc9ebbacbdea74a38de18e3/xxhash-3.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:538f5f865df6cd8c32dd63158a0e5b4f5dd08d732a7da8b7228a5a0776c8ce55", size = 32739, upload-time = "2026-07-06T10:45:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/bb51dbf7c363ff88a7cbd50b7959718219577ef44d7cf255929ffc4a2194/xxhash-3.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:a6617f30641ba0d8baa1635fbefb1dffc5165ec36d26921bd5cee13497cd937a", size = 29239, upload-time = "2026-07-06T10:45:53.714Z" }, + { url = "https://files.pythonhosted.org/packages/56/d3/827ca123c2ee5443a6aaed3c5dd199237dc2f010e2bebd7ec09ef36f3a5f/xxhash-3.8.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:bfcd82852c62a60e314670a9602de354c4460f8adad916e2e42a20860c7870bc", size = 34964, upload-time = "2026-07-06T10:45:55.535Z" }, + { url = "https://files.pythonhosted.org/packages/05/67/67ae2a3ccdeb8b8ef025d35aee9edd1d26c3abe5051d47da9286232afbf8/xxhash-3.8.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:08ea2081f5e88615fec8622a9f87fbe21b8ea58d88cfc02163ca11026ee62a92", size = 32697, upload-time = "2026-07-06T10:45:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/38/5a/3d3994346e1f45493679cb5c1ffc2bf454e410e9d1e8a662d253becee91e/xxhash-3.8.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2e32855b6f9e5b18f449e59d45e3d5778bdeb660632ef2693cca267a11246c75", size = 225954, upload-time = "2026-07-06T10:45:58.897Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2c/53169270309b7cd8e05504e07fe123bac053b89d00ac63617faacf0a2ec0/xxhash-3.8.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6e088bd7870775624256a0d84c2a6714afd223b2eeb56b0ca58398e52a32fda", size = 249776, upload-time = "2026-07-06T10:46:00.977Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/5c551d8d592f944506f7c5185e210255c15e672a3c6008c156a1bd9b775e/xxhash-3.8.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:72eb5ae575cc7ae2b23f6f8064a8b10f638c7149819ae9cc6d20ebd4d37a1629", size = 274776, upload-time = "2026-07-06T10:46:02.869Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/d3a762270cee2d7bcd0e25e28c623e5f3f5c0dc637b66e3e47dd5b0bb3f0/xxhash-3.8.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d0b48cdf690a64cedf7258c3dc9506cc41fc86edd7739c40e3098952265dc068", size = 252056, upload-time = "2026-07-06T10:46:04.688Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/b78e4373b2cb6d1c42af60ea2d7e9146ad0710b239ac7f706d5d31d5bb98/xxhash-3.8.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb9e256a357dfcede7818c6d34e70db2d6b664394803d1de4b6984d2de76c0f1", size = 482108, upload-time = "2026-07-06T10:46:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/642d923336ea61a15f8ce64fc7e078729e6e06c3a026e517fa79b2c23b7a/xxhash-3.8.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51f71a6e2ad071e70c937e41fcb6c19f82c3f9f49831eba850ed4a106ffbb647", size = 226739, upload-time = "2026-07-06T10:46:08.598Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0a/a37d6da6427d45a8d23e3ee3a0ca9c9d4a90364849c6637fe2963a755f9b/xxhash-3.8.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4a6443968c4e8dc69967e12776776a5952c119cc1bd94168ad1c5ad667c2be1", size = 319658, upload-time = "2026-07-06T10:46:10.504Z" }, + { url = "https://files.pythonhosted.org/packages/4a/51/ebbd40da8a3f1bc53b4b7a9a87f8e28bd95c5f21bc14b8a57860cf367d1b/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:714503083a1f2065c9ad15340dd49ac8a8e948a505a705ffa1750cb951519113", size = 246059, upload-time = "2026-07-06T10:46:12.634Z" }, + { url = "https://files.pythonhosted.org/packages/24/4c/d9014030147e1f0bb26e7da47aa240dd9ec61c763c573e558111d869f8e1/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:77f74e45a1e5574bbbf80181c8027b3a4c65c2248fffbd557bd596fff13102f9", size = 275535, upload-time = "2026-07-06T10:46:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/caee2db41fadcd5a25aa4323213f9afec5a8586d4e419241e3d659362bd7/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e0e1b0fb0259c1b75d1251ac0bb4d7ab675d36f7a6bf4ba6aa630dae94f9ffa", size = 231292, upload-time = "2026-07-06T10:46:16.452Z" }, + { url = "https://files.pythonhosted.org/packages/0b/60/f52f08bcdc904c4514ea5c25caa19e9f3214144434a6ff96dc82dc1cbddd/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:10e4393ec33633c2f05ad01869e546ad080b1a18f2650503731f153774608b31", size = 250490, upload-time = "2026-07-06T10:46:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/24/a0/94dc7ae310838f250669c6ad7168e6d6fca17d49dac1053f06dc232c4a56/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b3ba794c3d885803db6c3116686923f1ec13bc86e621e169a375282b63ea1cc6", size = 309861, upload-time = "2026-07-06T10:46:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/adeead7d0eb28cdfc2832544ea639ffbc6749ccde47a8e228d667459182e/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:57189a69c0891e4818853feaa521c972d22c880a001453addea015f48e3c3398", size = 448739, upload-time = "2026-07-06T10:46:22.79Z" }, + { url = "https://files.pythonhosted.org/packages/04/a4/22ec0e07db57d901c9298ae98aa3cf2be45bafded6f07c13131e85b89032/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d59e71153fe9ff85648d00e18649b07e9b22c797291abb7e27274fa06df8b838", size = 223657, upload-time = "2026-07-06T10:46:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/94/32/8a9531f37b59e5a013003db7cb7414baf4ce7e0e1268e0d5947cd3d6a2df/xxhash-3.8.1-cp313-cp313t-win32.whl", hash = "sha256:5b96f0024e9840f449bd91b2d005c921a4b666055a0d1b6492463799f32aae22", size = 32377, upload-time = "2026-07-06T10:46:26.86Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/2ca45fd7f671de5f81fc297ef1c95080b40c86ec6be0cc6034b8f7707ac8/xxhash-3.8.1-cp313-cp313t-win_amd64.whl", hash = "sha256:37d5a56c36dcc0b9a87b814cd992598d33863ff683749de6c86081f278d5e629", size = 33274, upload-time = "2026-07-06T10:46:28.39Z" }, + { url = "https://files.pythonhosted.org/packages/5a/54/20d7163463ddb6438b73a427d1655a77a502cf9b9b0c3ada3599629d9c0a/xxhash-3.8.1-cp313-cp313t-win_arm64.whl", hash = "sha256:6696c8752aded28ff3b16f33ef28ce28fb5d209b80c206746f943199fcf5fd65", size = 29375, upload-time = "2026-07-06T10:46:29.962Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/df2ba04f22a6cd6b39f96a6577329a8471a55c90ef8d8e2f7c102363613f/xxhash-3.8.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:9db455cb649dcfe4504d6d68a6d83a7315a99a3ca59871dc3ff840671f99adba", size = 38430, upload-time = "2026-07-06T10:46:31.496Z" }, + { url = "https://files.pythonhosted.org/packages/b2/4f/6a059e8ad3ca8deedc91dfe335b211204900895152212c03ebbe721de68b/xxhash-3.8.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:affb37f152e55b5e4494bb9d0107f7bb08515c6704fbed82d9f61214d74adc17", size = 36558, upload-time = "2026-07-06T10:46:33.078Z" }, + { url = "https://files.pythonhosted.org/packages/cb/95/40be178205acce092ae418feb20ac737b32a02c7b864926ed0717354c9f8/xxhash-3.8.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:460261045936975193bfd20549a0de1cd52a33b405cbb972f0d80940c42266cd", size = 31181, upload-time = "2026-07-06T10:46:34.793Z" }, + { url = "https://files.pythonhosted.org/packages/3f/89/2da4dbf051bafa156c0e3f12012db2b0ac3b84ff37ca1f021f6bfffcdfbb/xxhash-3.8.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:38c887aedb696ef8bca19983206d270848558cfae4a91afa6a2fb05dde58ffc5", size = 32192, upload-time = "2026-07-06T10:46:36.393Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4e/e000bbae3566bc8e0be771a8a0f294aa99075e3f0bc4ef43922ebffdebc8/xxhash-3.8.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:594131ce1aad18db3689781f806db1b065cdaa04f4df36b4c038d2013aefd0bf", size = 34691, upload-time = "2026-07-06T10:46:38.1Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4a/ea954aacc7d1c8711880ac2b55da94429a9b4296b151c4fc0966549ca1ee/xxhash-3.8.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:78c794b643d214f1522e7a288bcf5a2de120d26cd170516749a4009dc92722c9", size = 34807, upload-time = "2026-07-06T10:46:39.647Z" }, + { url = "https://files.pythonhosted.org/packages/ca/29/df598e738ff37558ac627264deb2e560902d9bf7f46d3bd5175c9eee593e/xxhash-3.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af0c9fedc4a2c24e8664953882fe8185f3790b8338c9c700f76f5ad660817711", size = 32410, upload-time = "2026-07-06T10:46:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/59/9c/81ab40e7d33ada0b3df5d1bc884894d15dbf4f805cd645b685e4606bb8e0/xxhash-3.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:115772daeb71b2f3b9381177017f53e6cf3f3439c840737fdabd21aba6e54920", size = 220564, upload-time = "2026-07-06T10:46:43.463Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6f/62ae6f5c8606320a0e2a41c2dc8c6d91cc5d63d0f84dd9582e9543779dd8/xxhash-3.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:000435984a0469b0f822fe76f35bddea0f96a4d6521b3339a60a6428cdee1edc", size = 241462, upload-time = "2026-07-06T10:46:45.509Z" }, + { url = "https://files.pythonhosted.org/packages/15/a1/9c3a0ec6cb524396f551eddd102a76690a795494eb9784fc67542b0daa37/xxhash-3.8.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2f1c68394818e0595569c2ff3cbc1e6d5a36a434e796f5c526b987b80c8a8c62", size = 264491, upload-time = "2026-07-06T10:46:47.655Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/700a4674e4308eb59d2fdb973977e82eae231bea5044753fee5c9eec0e0c/xxhash-3.8.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46b39976d008e2a845758650f0ff7136bca004f40da0c8798bd37ac37860154f", size = 242905, upload-time = "2026-07-06T10:46:49.857Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8a/72d9874375c8d4cbc64a8cd1d659d5695a8765c3db82efa82dc5bd9f14d0/xxhash-3.8.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d5006c65ec507a333479e76e00e2c368781f16c24ededa764763956b32a0e93e", size = 473873, upload-time = "2026-07-06T10:46:51.953Z" }, + { url = "https://files.pythonhosted.org/packages/03/f0/6db07590ed7e0a77f186ef0bcea8d52553bf1ba57833e09467a2411f0f2d/xxhash-3.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31a2649bcf1fe97cf11c79848d761df33ac46b3896942d31b640557b486ff6b", size = 220765, upload-time = "2026-07-06T10:46:55.41Z" }, + { url = "https://files.pythonhosted.org/packages/8f/10/00d12d8b8beabbf49a8bbc626fb9f40445145a8887eb41a6acfb69149ac4/xxhash-3.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f759eed402448c2bdbb492e4fba1f20668ffe29688605ea61f0f67f9e4e386d", size = 310478, upload-time = "2026-07-06T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f9/12a82394eefb0f185d15a7f7b9f627c61c475a72dd83718436a5b84b42ac/xxhash-3.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5f97ecfede10d5b2870383620e2d25c8561e217c7bf9081073802b54248d2b", size = 238393, upload-time = "2026-07-06T10:46:59.87Z" }, + { url = "https://files.pythonhosted.org/packages/20/f3/53f963e320b9ce678337aa7273f39ce692ded8b99e3d22a866ec722159ab/xxhash-3.8.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1da930bbcac3e8fbe2191850e2abb57977a99348c12c4b385e1058ac1b0a9ecc", size = 268704, upload-time = "2026-07-06T10:47:01.806Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/5b5badbd87c82d9f9b5f58ac74a3f29ef08f6fc387b324b8fd482450b862/xxhash-3.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:747476436f6891b9773374ce8d48edcc8b12cb5b61b67c6fb6289633747d088f", size = 225015, upload-time = "2026-07-06T10:47:03.784Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/3ca68265afe7b4e69435e08a7b6a1d9d0f2a071e889da1f8041ed00fe878/xxhash-3.8.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef09bbc2519a93cd0f95f2ceb5f7b85919dffea643278e02362bf40e3c4bed1", size = 240951, upload-time = "2026-07-06T10:47:05.816Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/27e19670c40f46b5e76e11f2f4713d21054804568425d870670e757172ad/xxhash-3.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a5eed9d41995a83f3332b4e3396abb7f433cac584222bd7e305b606d8353861e", size = 300751, upload-time = "2026-07-06T10:47:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fb/b33e27689959fe7ed2ae0b830af41560d65213943983afa9db3a8d481bce/xxhash-3.8.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:53f3ed9118397074ff63a79b66b7fec1c84c782eecde35c5bc94e420a971c231", size = 443480, upload-time = "2026-07-06T10:47:10Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/0e0d973be5fe280753ef02fbc89349492ad6e903bf1dcb870b668f94b662/xxhash-3.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d247b34bf433c92b41689318fd25d246313cab2275a6a47e2efac178b80d6efe", size = 217657, upload-time = "2026-07-06T10:47:12.196Z" }, + { url = "https://files.pythonhosted.org/packages/ad/68/c9e3ecef4a9a417d464cb5bd200aa12f73192dee677901b9e08e0ad0d1bb/xxhash-3.8.1-cp314-cp314-win32.whl", hash = "sha256:d58ce8b6cfa9c4d2f230557f69caf7c06369e318015d0b19485095bc2c5963ab", size = 32690, upload-time = "2026-07-06T10:47:14.204Z" }, + { url = "https://files.pythonhosted.org/packages/d7/99/e9e44588c0b62837bbec5ba7927816de0afa03406b1a0b6c7a7e1d1a30a0/xxhash-3.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:6cee733fe4ccb1737e0997135283c82341e5cfa9cf214b165f9087fb663aaf4f", size = 33460, upload-time = "2026-07-06T10:47:16.021Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/64f36d86380b3657ad9031967ab814f3ef31307174650853f69c18932ebc/xxhash-3.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:58346024d47e84f7d8b3e7f5d6faa1d58acbbe49a8771497872059f58c1d8ea5", size = 30092, upload-time = "2026-07-06T10:47:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/92/cb/18b64bff88c58a0ca209dc533e63cf02d7ae5aa6b1b9a9fd14e81b5dbd60/xxhash-3.8.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:01cab782f8a0a05ecad2c63d7ef10f7ab475f660e0d6419d069418c14d88de7c", size = 35024, upload-time = "2026-07-06T10:47:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/af/1d/72d8a70520e5dcddb472ea0486d299da3240745a10658290cd7b5690ede2/xxhash-3.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:717b12fdc51819833704e85e6926d76981ffa3f780ef92e33ebb8b26d46bb230", size = 32697, upload-time = "2026-07-06T10:47:21.649Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b8/e041f555903c56db3d0a731b3d72a6575d75e0ed868b1bd2e5176111ca44/xxhash-3.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ec55d80e9b8a519d742669e0b49e8ce9e6747be42bf3c138158b6543a9c8e489", size = 226044, upload-time = "2026-07-06T10:47:23.612Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7e/5cdcf06bf6ec4b5d2ac073feb23432ec1d603fd438864cbd2c09c7cb45e1/xxhash-3.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98d8ac1129b4dd39098cffed94d1284aceb61c3aa396757ccc736ac392e4cee5", size = 249899, upload-time = "2026-07-06T10:47:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c0/eb7e059cb5e1dba11fd30d2fdf882f56e5a417a3eaa43669d43623767f45/xxhash-3.8.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3bc0fa90830df1e1277f33cc6e55de9990b83c0319fd8c7412866cfde38b025e", size = 274892, upload-time = "2026-07-06T10:47:27.931Z" }, + { url = "https://files.pythonhosted.org/packages/66/74/a600aaf7cd39957fd1510adeedb1749c1e7eb82bd632a1153d9c664c3135/xxhash-3.8.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c73b6f652f0745425aa6378319c331293b5341756262e9408ed3d45f183375e6", size = 252243, upload-time = "2026-07-06T10:47:30.288Z" }, + { url = "https://files.pythonhosted.org/packages/ad/04/78d88fa75a6763e5d09bf1b947a392a27988903381b219006f92f3c68fc8/xxhash-3.8.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6114692261eff4266386cdec0f7d87eee24e317ab397c218b7ae6a76b4c6339", size = 482191, upload-time = "2026-07-06T10:47:32.45Z" }, + { url = "https://files.pythonhosted.org/packages/7f/06/07a8aea1108d682de8791ce608cdf367d75ff4e7e57cd3c154bdc6f47b23/xxhash-3.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df57c0b161ec1b3ed0526a67b0db0914b557e86ee8aae51887aec941b261542", size = 226877, upload-time = "2026-07-06T10:47:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b5/86bade5618a524d2c06c4041aa2fe8e5749ce16e88afba60d67c1684a21f/xxhash-3.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9043877a917be88ccf230aa5667c1bd059bce80f4c2727e4defa1b29b7f48b08", size = 319794, upload-time = "2026-07-06T10:47:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/23/69/9b1a2b89b1621bb740fbcb7beb512f60f99480c1bdc680c0c90e1f56ff75/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:559e3cabe522231909f9de98ef06929edbd53782046bd21aae0c72db6f2a0775", size = 246202, upload-time = "2026-07-06T10:47:39.676Z" }, + { url = "https://files.pythonhosted.org/packages/08/ea/662ed6cb49f1d34078b6a3a3e0f3d29ff93fd7b5a03c0bc9ecfd9b2159c3/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:264710bd335016f303763ce1275c6486df30bb57c2245c91b224c983d7ac39b8", size = 275628, upload-time = "2026-07-06T10:47:41.99Z" }, + { url = "https://files.pythonhosted.org/packages/13/f5/49fc9e4c6728a5a3bd8fe639199d2fa67609b3a84f938aff6e8568dd3e4f/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e14800b9b10bb39d7a60ad4a310e403164d7b8988a27ae933d4e40618a44088e", size = 231390, upload-time = "2026-07-06T10:47:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/64/9d/3acaf8f599c0e0b30e910a3a11ba32929da53c86dc73c7c55fe6a010b4e9/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ea6a3e734b0fd41b82784a400be946821900daebe610c050a5e0760838a34f99", size = 250600, upload-time = "2026-07-06T10:47:47.611Z" }, + { url = "https://files.pythonhosted.org/packages/23/64/8acab4c5ec60dbe664b5b9858fd44c2413b07e535b09556a0a5022e78aa6/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cf399fac542a1c7a4734a435b93df2c55e858c7d31abf6c1bdf46f9ae67fbfd0", size = 310032, upload-time = "2026-07-06T10:47:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/56/47/a0288d7329b1fe63e2734a32d19d444a96ae2b4810f545bc61e561224917/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:44c89d915a75c11d2547eaee9098fcd80398987c4bff2974a0497a925bf92c07", size = 448882, upload-time = "2026-07-06T10:47:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/01/e7/3071dfd3beb5c38204ce1cf56bf7749fce08de900fa92714b81d1d8ca1f2/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:358650d5bda9c635da699c53adf4e8134af492ecc79c960f917eebf088bb6799", size = 223728, upload-time = "2026-07-06T10:47:55.093Z" }, + { url = "https://files.pythonhosted.org/packages/12/11/b99949f0ba2b07e9f9ffe83b9c86faa685f9080725dc21a916a607313be5/xxhash-3.8.1-cp314-cp314t-win32.whl", hash = "sha256:c240939e963653054fc7e4a17c382829cda4aa88a7daf0af841715dbded1b497", size = 33150, upload-time = "2026-07-06T10:47:57.274Z" }, + { url = "https://files.pythonhosted.org/packages/54/1c/09703eb341f8416e74e58d6c6732d4b5c46de59c942363203cb237cc95b0/xxhash-3.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7258ee276e8772599bc19e14b36f6260306e21b637190cd7cb489a2449d48684", size = 34005, upload-time = "2026-07-06T10:47:59.434Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f9/6ed7251bb6a8af10ac73b1821c60583d2826e5b2064e45a979c935287c98/xxhash-3.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:8f454166c2ffed45636c8d501741e649851ba2f346c4eb73a64c07ac00428f20", size = 30239, upload-time = "2026-07-06T10:48:01.874Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/4d8040435aeac814fc69ba63621565fbeb19229a138e2568324a26b2a45c/xxhash-3.8.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:39c9d5b61508b0bb68f29e54546de0ed2a74943c6a18585535a7e37356f1dd12", size = 32687, upload-time = "2026-07-06T10:49:42.803Z" }, + { url = "https://files.pythonhosted.org/packages/da/6a/975f1f2318c760e5bcec109ed379713ae645d8d856c2a3b9ec5d26857087/xxhash-3.8.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:83b9130b80b216d56fdf9e87131946b353c9627930c061955a101ea82b09fed9", size = 29879, upload-time = "2026-07-06T10:49:45.172Z" }, + { url = "https://files.pythonhosted.org/packages/08/0b/40a2a55ff52cf635bfdc5eae67a772bec85b4f44c6c737f73f6f528d51d1/xxhash-3.8.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8304be0982130954b7fd3aad18e2c6f8ee40254bc3d2e635991c16d77c91e2bd", size = 43246, upload-time = "2026-07-06T10:49:47.905Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/56ed2b6b200f26fb474f3fd387d95d0601efcd5bb33430c90c68924bdd77/xxhash-3.8.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b512261801b1e5fde7b6ebf2fef7977339c620cbbca88a0040ad9ad134f4d02", size = 38202, upload-time = "2026-07-06T10:49:50.59Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a3/56864d895d1161a9f17502088e9c1fb7c06bde2c2efdde620d22bb7a9c43/xxhash-3.8.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49aa8692507835dcc1e8ad8021f20c74c2dc13d83b5112e87877faa2a0035b20", size = 34448, upload-time = "2026-07-06T10:49:53.242Z" }, + { url = "https://files.pythonhosted.org/packages/6b/57/5c6e0908a47f61dca96d01c8ee6fce01ed1050611eb779083ba8758fed81/xxhash-3.8.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:345b07b78e2bf583d71682aa34ae5b5fab575f7a1cb31e10263ebbc6f89f8c42", size = 32869, upload-time = "2026-07-06T10:49:55.972Z" }, +] + [[package]] name = "zipp" version = "4.1.0" @@ -1389,3 +2087,77 @@ sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0 wheels = [ { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, ] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]