diff --git a/AGENTS.md b/AGENTS.md index 8b0e249..92eeb6b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,27 +45,34 @@ practice, the package covers both supervision and governance. ### Current Roadmap Focus -The current build focus is the v0.1 runtime: LangGraph adapter first, then the -deterministic policy/risk Decision Gate. Work in this repo should reinforce -that sequencing rather than jumping ahead to later judge- or multi-framework -features. +v0.1 (Sidecar runtime, LangGraph adapter, deterministic policy/risk Decision +Gate, Observe mode) and v0.2 (Intent Guardian, Govern mode) have shipped. +The current build focus is v0.2.x, the narrow Early Validation Benchmark +(see ROADMAP.md). Work in this repo should reinforce that sequencing rather +than jumping ahead to Planner/Critic/Judge (v0.3) or multi-framework +features (v0.6). ### Before You Build Here - Ask whether the feature is a pre-action governance concern; if it is retrospective analysis, it likely belongs in `agenticlens` instead -- Keep v0.1 deterministic where the roadmap says it should be deterministic; - do not solve early gate behavior with model-based evaluators +- Keep the Decision Gate deterministic where the roadmap says it should be + (`gate/`, `intent/`); do not solve that behavior with model-based + evaluators - Avoid designing sidecar abstractions as if all frameworks are already supported; the first real adapter is still shaping the boundary ## Status -This repository is a **scaffold** — directory layout, tooling config, and -CI/release workflows exist; `src/agentic_sidecar/` modules are placeholders -(docstring only, `NotImplementedError` on any callable if one exists) until -their version lands. See [ROADMAP.md](ROADMAP.md) for the build order before -adding real logic to any module. +v0.1 and v0.2 are implemented: `core/` (`Sidecar`, `Decision`, +`DecisionContext`, `operators.py`, `exceptions.py`), `gate/policy.py`, +`gate/risk.py`, `intent/` (`IntentEnvelope`, `IntentGuardian`, +`ConstraintBinding`), and `adapters/langgraph.py` (both Observe and Govern +mode) have real code and tests. Everything else under +`src/agentic_sidecar/` (`evaluators/`, `status/`, `cli/`, `gate/budget.py`, +the remaining `adapters/*.py`, `integrations/*.py`) is still a placeholder +(docstring only) until its version lands. See [ROADMAP.md](ROADMAP.md) for +the build order before adding real logic to any of those. ## Build and Run @@ -88,15 +95,16 @@ adding real logic to any module. ## Design Constraints These are load-bearing, not preferences — see -[ROADMAP.md § Design Constraints](ROADMAP.md#design-constraints-read-before-building-v01) +[ROADMAP.md § Design Constraints](ROADMAP.md#design-constraints-read-before-building-v01v02) for the full rationale on each: 1. **One framework adapter first.** `adapters/langgraph.py` before any of the other four. Do not claim framework independence until a second adapter has been built against real usage. -2. **Rules before models.** v0.1's `gate/policy.py` and `gate/risk.py` must - work with zero LLM calls. Don't reach for `evaluators/judge.py` (v0.3) - to solve a v0.1 problem. +2. **Rules before models.** `gate/policy.py`, `gate/risk.py`, and (v0.2) + `intent/alignment.py` must all work with zero LLM calls. Don't reach for + `evaluators/judge.py` (v0.3) to solve a problem these can answer + deterministically. 3. **`on_sidecar_failure` has no default.** Every Decision Gate path must handle `fail_open` and `fail_closed` explicitly — this is a governance property, not an implementation detail. @@ -111,18 +119,18 @@ for the full rationale on each: ## Repo Map -| Path | Purpose | Planned version | +| Path | Purpose | Version | |------|---------|------------------| -| `src/agentic_sidecar/core/` | `Sidecar` class, `attach()`, decision-boundary interception, `Decision` type | v0.1 | -| `src/agentic_sidecar/gate/policy.py` | Policy Advisor — deterministic YAML allow/deny rules | v0.1 | -| `src/agentic_sidecar/gate/risk.py` | Risk Evaluator — rule-based classification | v0.1 | +| `src/agentic_sidecar/core/` | `Sidecar` class (`evaluate()`, `before_tool_call` hook), `Decision` type, `DecisionContext`. `attach()` is *not* here — see AGENTS.md's Package Boundaries, adapters own it | v0.1 (implemented) | +| `src/agentic_sidecar/gate/policy.py` | Policy Advisor — deterministic YAML allow/deny rules | v0.1 (implemented) | +| `src/agentic_sidecar/gate/risk.py` | Risk Evaluator — rule-based classification | v0.1 (implemented) | | `src/agentic_sidecar/gate/budget.py` | Budget Guardian — cost/token ceilings | v0.4 | -| `src/agentic_sidecar/adapters/langgraph.py` | LangGraph interception adapter | v0.1 | -| `src/agentic_sidecar/intent/` | `IntentEnvelope`, alignment scoring, drift detection | v0.2 | +| `src/agentic_sidecar/adapters/langgraph.py` | LangGraph interception adapter, incl. `attach(sidecar, tools)`; enforces `BLOCK` in Govern mode (v0.2) | v0.1/v0.2 (implemented) | +| `src/agentic_sidecar/intent/` | `IntentEnvelope`, `IntentGuardian`, `ConstraintBinding` — constraint validation only, no `authority` enforcement yet | v0.2 (implemented) | | `src/agentic_sidecar/evaluators/planner.py` | Planner — evaluates the whole plan against intent | v0.3 | | `src/agentic_sidecar/evaluators/critic.py` | Critic mode — pre-decision challenge | v0.3 | | `src/agentic_sidecar/evaluators/judge.py` | Model-agnostic LLM Judge interface | v0.3 | -| `src/agentic_sidecar/gate/` (full outcome set) | `WARN` / `CHALLENGE` / `REPLAN` / `PAUSE` / `ESCALATE`, human-in-the-loop escalation | v0.4 | +| `src/agentic_sidecar/gate/` (remaining outcomes) | `CHALLENGE` / `REPLAN` / `PAUSE` / `ESCALATE`, human-in-the-loop escalation (`ALLOW`/`WARN`/`BLOCK` shipped in v0.1/v0.2) | v0.4 | | `src/agentic_sidecar/status/narrate.py` | Human-readable status narration | v0.5 | | `src/agentic_sidecar/cli/` | CLI entry point (`agentic-sidecar status --follow`) | v0.5 | | `src/agentic_sidecar/adapters/{crewai,autogen,openai_agents,google_adk}.py` | Additional framework adapters | v0.6 | @@ -145,10 +153,17 @@ Full architecture and build order: [ROADMAP.md](ROADMAP.md). - AgenticLens integration is optional (`agentic_sidecar.integrations.agenticlens`) and must auto-skip in tests if `agenticlens` is not installed. - `core/` must not import from `adapters/` (adapters depend on core, not the - reverse). + reverse). `core/` *does* import from `gate/` and `intent/` directly + (`Sidecar` wires in Policy Advisor, Risk Evaluator, and Intent Guardian as + built-in Decision Gate modules) — that's the expected direction, not an + exception to this rule. The distinction: `gate/`/`intent/` are modules + Sidecar orchestrates itself; `adapters/` are alternate entry points into + Sidecar, one per framework, and core has no business knowing any of them + exist. - `gate/` (Policy, Risk) must work with zero dependency on `evaluators/` - (Planner, Critic, Judge) — v0.1's Decision Gate has to function before - Judge exists at all. + (Planner, Critic, Judge) or `intent/` — v0.1's Decision Gate has to + function before Judge or Intent Guardian exist at all, and Policy/Risk + answer different questions than Intent (see Design Constraint 5). - `evaluators/judge.py` must stay model-agnostic — no hardcoded provider SDK imports at module scope. diff --git a/CHANGELOG.md b/CHANGELOG.md index 356e6f5..1cb1119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,156 @@ All notable changes to this project are documented here. ## [Unreleased] +## [0.2.0] - 2026-08-15 + +### Added + +- Intent Guardian (`agentic_sidecar.intent`): `IntentEnvelope` (goal, + requester, constraints, authority, expiry — concept.md §6), `Requester`, + `ConstraintBinding` (binds one envelope constraint to a specific tool + argument and comparison op), `IntentGuardian` (mirrors + `PolicyAdvisor`/`RiskEvaluator`'s construction shape), and + `evaluate_alignment()`. Scope is deliberately narrow: constraint + validation only (numeric/enum/allow-list, e.g. `maximum_refund: 500` vs. + a proposed `850`) and envelope-expiry detection. `authority` is carried + on the envelope (matching concept.md §6's shape, for future + `ai-operations-spec` alignment) but has no binding/enforcement mechanism + yet — deferred for the same reason Design Constraint 4 defers a + model-based risk classifier: build it once a real scenario motivates the + shape, not speculatively. +- `WARN` added to `Decision.status` (previously `ALLOW`/`BLOCK` only) — + Intent Guardian's outcome for a finding worth surfacing (a stale/expired + envelope) but not severe enough to block. +- Govern mode (`Sidecar(mode="govern")`): a `BLOCK` decision is now + actually enforced. `agentic_sidecar.core.exceptions.SidecarBlockedError` + is raised by an adapter (not by `Sidecar.evaluate()` itself, which only + ever computes a `Decision` — see its docstring) when Govern mode's + `BLOCK` should stop the call. `agentic_sidecar.adapters.langgraph.attach` + now raises it instead of calling the real tool when + `sidecar.mode == "govern"` and the decision is `BLOCK`; `WARN` and + `ALLOW` still call through in both modes. +- `Sidecar.set_intent(guardian)` — swaps the active `IntentGuardian` + between tasks (an `IntentEnvelope` is meant to be per-task, concept.md + §6, not fixed for a Sidecar's whole lifetime). Raises if `intent=...` is + given (at construction or via `set_intent`) without `"intent_guardian"` + in `roles` — the same "no silent gap" principle `on_sidecar_failure` + already applies, extended to this footgun. +- `"intent_guardian"` is now a supported `Sidecar` role (previously raised + `NotImplementedError` naming v0.2). +- `core/context.py`: `IntentSnapshot` (goal + constraints only — the + lightweight view attached to `DecisionContext.intent`, not the full + `IntentEnvelope`, which `core/` does not depend on) and `HistoryEntry`. + `Sidecar.evaluate()` now injects both into every `DecisionContext` before + dispatching to an evaluator (concept.md §7, Intent Injection), built from + `self.decisions` and the active `IntentGuardian` automatically. +- `core/operators.py`: shared `ArgOp` + `compare()`, extracted so + `gate/risk.py`'s argument-pattern rules and `intent/alignment.py`'s + constraint bindings don't duplicate identical comparator logic. + `gate/risk.py` refactored to use it; its own rule-matching behavior is + unchanged. +- `examples/langgraph_intent_guardian_govern_mode.py` — the refund-limit + scenario from concept.md §9 end to end against a real + `langgraph.prebuilt.create_react_agent` agent: an $850 refund request + raises `SidecarBlockedError` before the real tool runs; a $120 request + goes through normally. +- Test suite extended: `test_operators.py`, `test_envelope.py`, + `test_alignment.py`, `test_exceptions.py`, plus new coverage in + `test_sidecar.py` and `test_langgraph_adapter.py` for Govern mode, the + intent/role consistency checks, and injected intent/history. + +### Fixed + +- `risk_block_threshold` was never validated at construction — an + unrecognized value (e.g. a typo like `"SEVERE"`) passed straight through + `Sidecar.__init__` and only surfaced as a `KeyError` deep inside + `evaluate()`, silently resolved via `on_sidecar_failure` instead of + failing fast. A misconfigured threshold could therefore fail open on a + genuinely high-risk action. Now validated against `RISK_ORDER` at + construction, raising immediately. +- `IntentEnvelope.is_expired()` raised an unhandled `TypeError` for a naive + (no `tzinfo`) `expires` value — a realistic input shape from YAML or a + caller that forgot `tzinfo=timezone.utc` — comparing it against the + timezone-aware `datetime.now(timezone.utc)`. Inside `evaluate()` that + error was swallowed into `on_sidecar_failure`'s fallback instead of + producing the deterministic intent-expiry `WARN` it should have. Now + rejected explicitly, at `IntentEnvelope` construction (a `field_validator` + on `expires`) and in `is_expired()`'s own `now=` parameter, with an error + naming the fix rather than a bare `TypeError`. +- `SidecarBlockedError`, raised by the LangGraph adapter in Govern mode, + carried the bare pre-evaluation `DecisionContext` the adapter built, not + the one Intent Guardian actually evaluated — `Sidecar.evaluate()` + injected `intent`/`history` into a *copy* (`context.model_copy(...)`) + rather than the object the caller held, so `SidecarBlockedError.context` + always had `intent=None`, even when an intent-drift finding was exactly + why the call was blocked. `evaluate()` now injects by mutating the + caller's `DecisionContext` in place (documented as intentional on the + model itself — it's why `DecisionContext`, unlike `Decision`, isn't + frozen), so any caller's own reference — not just `sidecar.decisions` — + reflects the fully-evaluated context once `evaluate()` returns. +- ROADMAP.md's v0.2 deliverable cited concept.md §22 (the DEV/production + cleanup scenario, actually used by the v0.2.x benchmark) for the + refund-limit worked example; corrected to §9, which is where that + scenario actually appears. + +## [0.1.0] - 2026-08-15 + +### Added + +- Sidecar runtime (`agentic_sidecar.core`): `Sidecar`, `Decision(status, + risk, reason)`, `DecisionContext`. `on_sidecar_failure: fail_open | + fail_closed` is a required setting with no default; both paths are + tested. `roles` validates against v0.1's supported set (`policy`, `risk`) + and raises `NotImplementedError` (naming the version) for roles planned + but not yet built, rather than silently ignoring them. +- Decision Gate (`agentic_sidecar.gate`): YAML-driven Policy Advisor + (`policy.py`, allow/deny rules by tool-name glob) and rule-based Risk + Evaluator (`risk.py`, tool-name glob plus an optional argument-pattern + check). Zero LLM calls, per ROADMAP.md's Design Constraint 2. +- LangGraph adapter (`agentic_sidecar.adapters.langgraph.attach`): wraps a + list of tool callables so every call is evaluated by a `Sidecar` first. + v0.1 ships Observe mode only — the wrapped call always executes; nothing + yet enforces a `BLOCK`. No import-time dependency on the `langgraph` + package itself; a `langgraph` extra is declared in `pyproject.toml` for + code that actually builds a graph around the wrapped tools. +- `examples/langgraph_refund_observe_mode.py` — a runnable, offline + (no API key) example against a real `langgraph.prebuilt.create_react_agent` + agent, demonstrating a Policy Advisor deny rule and a Risk Evaluator + argument-threshold rule both firing in Observe mode. +- Test suite covering `Decision`, `DecisionContext`, `PolicyAdvisor`, + `RiskEvaluator`, `Sidecar` (including both `on_sidecar_failure` paths), + and the LangGraph adapter. + +### Changed + +- `Sidecar.attach(agent)` from README.md's Planned Python API is *not* how + v0.1 actually ships attach: `core/` must not import from `adapters/` + (AGENTS.md's Package Boundaries), so wrapping a specific framework's + tool-call surface is each adapter's own function + (`agentic_sidecar.adapters.langgraph.attach(sidecar, tools)`) rather than + a generic method on `Sidecar`. README.md documents both the real v0.1 + shape and the longer-term generic shape this is expected to grow toward + once a second adapter exists (Design Constraint 1). + +### Fixed + +- ROADMAP.md's Release Status/summary described v0.1 as "Rule-Based + Decision Gate" without noting it's Observe-mode-only (advisory logging, + not enforcement) — a reader skimming just the top could overestimate + what shipped. Now says so explicitly in both places. +- ROADMAP.md's Package Layout diagram listed a `semantica.py` placeholder + under `integrations/` as if it already existed, alongside `agenticlens.py` + and `agentic_chaos.py`, which do. It doesn't yet (no code, no + `pyproject.toml` extra) — it's a v0.6 deliverable; the diagram now says + so. Same diagram also still described `core/sidecar.py` as owning + `attach()`, which isn't how v0.1 actually shipped it (see "Changed" + above) — corrected to match. +- ROADMAP.md's Design Constraints section header said "these four" while + listing five items. +- ROADMAP.md's top summary said v0.2-onward modules were "still unstarted" + when docstring-only placeholders for several of them already exist in + the tree — reworded to distinguish "no real logic yet" from "doesn't + exist yet." + ## [0.0.1] - 2026-08-12 ### Added @@ -34,6 +184,3 @@ All notable changes to this project are documented here. ### Changed - Renamed `future-plans.md` to `concept.md` and updated every reference. - -No functional code has shipped yet — see [ROADMAP.md](ROADMAP.md) for the -v0.1 scope (Sidecar Runtime + Rule-Based Decision Gate). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 21ab377..4ce4b56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,8 @@ Thanks for helping make `agentic-sidecar` better for everyone building governable autonomous AI agents. -`agentic-sidecar` is currently a **scaffold** — see +`agentic-sidecar` has v0.1 (Sidecar Runtime + Rule-Based Decision Gate) +implemented; everything from v0.2 onward is still a **scaffold** — see [ROADMAP.md](ROADMAP.md) for what's planned and in what order before starting on a module. diff --git a/README.md b/README.md index 165d65b..1a41db6 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,14 @@ ## Status -**Concept / pre-implementation.** This repository currently contains the -architecture proposal ([`concept.md`](concept.md)) and this README -— no package code, no PyPI release, no CI yet. See -[ROADMAP.md](ROADMAP.md) for the build plan, starting with a narrow, -LLM-free v0.1. +**v0.1 and v0.2 implemented.** The Sidecar runtime, a rule-based Decision +Gate (Policy Advisor + Risk Evaluator), and Intent Guardian (`IntentEnvelope` ++ constraint validation) are real code, attached via the LangGraph adapter. +Observe mode (v0.1, logs only) and Govern mode (v0.2, a `BLOCK` is actually +enforced) both work today — see [Python API +(implemented)](#python-api-implemented) below and [`examples/`](examples/) +for two runnable scripts. No PyPI release yet. See [ROADMAP.md](ROADMAP.md) +for the full build plan (v0.2.x onward). ## Contents @@ -21,6 +24,7 @@ LLM-free v0.1. - [Main Agent vs. Sidecar](#main-agent-vs-sidecar) - [How the Decision Gate evaluates a decision](#how-the-decision-gate-evaluates-a-decision) - [Sidecar modules](#sidecar-modules) +- [Python API (implemented)](#python-api-implemented) - [Planned Python API](#planned-python-api) - [Operating modes](#operating-modes) - [Long-term: an intent propagation layer](#long-term-an-intent-propagation-layer) @@ -50,7 +54,7 @@ Three things go wrong in that chain that permission checks alone don't catch: `agentic-sidecar` is designed to attach to any agent framework, but the first release proves that against one framework only — LangGraph — before claiming -the rest (see [Design Constraints](ROADMAP.md#design-constraints-read-before-building-v01) +the rest (see [Design Constraints](ROADMAP.md#design-constraints-read-before-building-v01v02) in ROADMAP.md). Either way, it answers a different question than permission checks or observability tools do: @@ -231,15 +235,20 @@ one generic "policy engine": Policy and Risk are largely mechanical — permission lists, thresholds, tool-argument patterns — and v0.1 ships them with zero LLM calls (see -[ROADMAP.md](ROADMAP.md)). Intent is the one that requires understanding -what the user actually meant, and it's where the project's distinctive -value lives. - -The diagram above shows the target outcome set. **v0.1 ships a narrower -slice**: only `ALLOW`/`BLOCK`, in Observe mode — the Sidecar logs what it -*would* have decided but cannot yet stop an action in practice. `WARN`, -`CHALLENGE`, `REPLAN`, `PAUSE`, and `ESCALATE` arrive across v0.2–v0.4 as -Intent Guardian and the full Decision Gate ship. Version-by-version build +[ROADMAP.md](ROADMAP.md)). Intent (v0.2) is still deterministic, not +LLM-based, but is the one that gets closest to "what the user actually +meant" so far, via structured `IntentEnvelope` constraints rather than +free-form judgment — full semantic understanding is further out on the +roadmap. + +The diagram above shows the target outcome set. **v0.1 shipped a narrower +slice** (`ALLOW`/`BLOCK` only, Observe mode: the Sidecar logs what it +*would* have decided but cannot stop an action in practice). **v0.2 adds +`WARN`** (an Intent Guardian finding worth surfacing but not severe enough +to block, e.g. a stale envelope) **and Govern mode**, where a `BLOCK` is +actually enforced by the attached adapter — see +`agentic_sidecar.core.exceptions.SidecarBlockedError`. `CHALLENGE`, +`REPLAN`, `PAUSE`, and `ESCALATE` remain v0.4. Version-by-version build order is in [ROADMAP.md](ROADMAP.md#build-order). ## Sidecar modules @@ -287,10 +296,104 @@ sidecar: on_sidecar_failure: fail_closed # or fail_open — see ROADMAP.md ``` +## Python API (implemented) + +v0.1 (Policy Advisor + Risk Evaluator, Observe mode): + +```python +from agentic_sidecar import Sidecar +from agentic_sidecar.adapters.langgraph import attach +from agentic_sidecar.gate.policy import PolicyAdvisor, PolicyRule +from agentic_sidecar.gate.risk import RiskEvaluator, RiskRule + +sidecar = Sidecar( + on_sidecar_failure="fail_closed", # required, no default -- see Design Constraints + policy=PolicyAdvisor([PolicyRule(tool="delete_*", effect="deny")]), + risk=RiskEvaluator( + [RiskRule(tool="issue_refund", arg_name="amount", arg_op="gt", arg_value=500, risk="HIGH")] + ), +) + +# Wrap the tools you'd otherwise pass straight to +# langgraph.prebuilt.create_react_agent(model, tools=...) -- every call is +# evaluated by the Sidecar first. +tools = attach(sidecar, [read_order, issue_refund, delete_customer_record]) +agent = create_react_agent(model, tools=tools) + +# mode="observe" is the default: agent.invoke(...) still executes every +# tool call. Nothing is enforced -- inspect what the Sidecar would have +# decided: +for context, decision in sidecar.decisions: + print(decision.status, decision.risk, context.tool_name, decision.reason) +``` + +v0.2 adds Intent Guardian and Govern mode, where `BLOCK` is actually +enforced -- the refund-limit scenario from concept.md §9: + +```python +from agentic_sidecar import Sidecar, SidecarBlockedError +from agentic_sidecar.adapters.langgraph import attach +from agentic_sidecar.intent.alignment import ConstraintBinding, IntentGuardian +from agentic_sidecar.intent.envelope import IntentEnvelope, Requester + +envelope = IntentEnvelope( + goal="refund_customer", + requested_by=Requester(type="human", id="user123"), + constraints={"maximum_refund": 500}, +) +binding = ConstraintBinding( + constraint="maximum_refund", tool="issue_refund", arg_name="amount", op="lte" +) + +sidecar = Sidecar( + on_sidecar_failure="fail_closed", + roles=["policy", "risk", "intent_guardian"], + intent=IntentGuardian(envelope, [binding]), + mode="govern", # a BLOCK now actually stops the call +) +tools = attach(sidecar, [issue_refund]) +agent = create_react_agent(model, tools=tools) + +try: + agent.invoke({"messages": [("user", "Refund order A100 for $850.")]}) +except SidecarBlockedError as exc: + print(exc) # "Sidecar blocked 'issue_refund'(...): Intent Guardian: ..." +``` + +`sidecar.set_intent(guardian)` swaps the active envelope between tasks -- +`IntentEnvelope`s are meant to be per-task (concept.md §6), not fixed for a +Sidecar's whole lifetime. + +`Sidecar.before_tool_call` registers a custom hook that fully replaces the +default Policy + Risk + Intent Guardian evaluation: + +```python +@sidecar.before_tool_call +def evaluate_action(context): + if context.tool_name == "delete_customer_record": + return Decision(status="BLOCK", risk="HIGH", reason="never allowed") + return Decision(status="ALLOW", risk="LOW", reason="ok") +``` + +Note the shape: `agentic_sidecar.adapters.langgraph.attach(sidecar, tools)`, +not `sidecar.attach(my_agent)`. `core/` intentionally has zero dependency on +`adapters/` (see AGENTS.md's Package Boundaries), so wrapping a specific +framework's decision boundaries is the adapter's job, not a generic method +on `Sidecar`. The "Planned Python API" below is the longer-term target this +is expected to grow toward once framework independence has actually been +proven (ROADMAP.md's Design Constraint 1) — not a regression. + ## Planned Python API -This is the target developer experience — **not yet implemented** (tracked -as v0.1 in [ROADMAP.md](ROADMAP.md)): +This is the longer-term target developer experience once a second framework +adapter exists and `roles` covers Planner/Critic too (v0.3) — +**not yet implemented**. `roles=["intent_guardian", "policy", "risk"]` and +`Sidecar(...)` construction already work today (see [Python API +(implemented)](#python-api-implemented) above); what's still aspirational +here is specifically `sidecar.attach(my_agent)` wrapping the *whole* agent +generically (today it's per-adapter, e.g. +`agentic_sidecar.adapters.langgraph.attach`) and `sidecar.evaluate(intent=, +action=, risk=)`'s keyword-argument call shape: ```python from agentic_sidecar import Sidecar @@ -323,10 +426,10 @@ Decision( | Mode | Behavior | | --- | --- | -| **Observe** | Sidecar monitors and logs; cannot affect execution. | +| **Observe** | Sidecar monitors and logs; cannot affect execution. **Implemented (v0.1).** | | **Advise** | Sidecar returns a recommendation; the agent decides whether to follow it. | -| **Govern** | Sidecar's Decision Gate can allow, warn, replan, pause, or block. | -| **Human-supervised** | High-risk decisions route to a human for approve/reject. | +| **Govern** | Sidecar's Decision Gate can allow, warn, or block for real. **Implemented (v0.2)** — `replan`/`pause` outcomes arrive at v0.4. | +| **Human-supervised** | High-risk decisions route to a human for approve/reject. Planned v0.4. | Modes are meant to be adopted in that order — organizations start in Observe and move to Govern once they trust the signal. diff --git a/ROADMAP.md b/ROADMAP.md index d495424..643bb61 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,8 +2,8 @@ ## Release Status -- **v0.1** 🚧 Planned — Sidecar Runtime, Policy Advisor & Rule-Based Decision Gate (no LLM required) -- **v0.2** 🚧 Planned — Intent Guardian (Intent Envelope, drift detection) +- **v0.1** ✅ Shipped — Sidecar Runtime, Policy Advisor & Rule-Based Decision Gate, **Observe mode only** — advisory logging, does not yet block (no LLM required) +- **v0.2** ✅ Shipped — Intent Guardian (Intent Envelope, constraint validation), **Govern mode** — `BLOCK` is now actually enforced - **v0.2.x** 🚧 Planned — Early Validation Benchmark (narrow, deterministic — intent-drift catch rate only) - **v0.3** 🚧 Planned — Planner, Critic & Judge (optional, model-agnostic LLM evaluation) - **v0.4** 🚧 Planned — Full Decision Gate outcomes, Human Escalation, Budget Guardian @@ -13,9 +13,17 @@ - **v0.8** 🚧 Planned — Evaluation Framework & Benchmarks - **v1.0** 🚧 Planned — Intent Envelope as a published interoperability schema -Nothing has shipped yet — this repo currently holds the architecture proposal -([`concept.md`](concept.md)) and this roadmap. Treat every -checkbox below as unstarted. +v0.1 and v0.2 have shipped (see the Build Order below). v0.1 introduced +Observe mode: the Decision Gate computes and logs every verdict but nothing +enforces it. v0.2 adds Intent Guardian (constraint validation against an +`IntentEnvelope`, `WARN` as a third outcome alongside `ALLOW`/`BLOCK`) and +Govern mode, where a `BLOCK` is now actually enforced by the attached +adapter — an action can genuinely be stopped, not just logged. Everything +scoped to v0.2.x and later remains a placeholder (docstring-only modules +already sit in the tree per the Package Layout below, but hold no real +logic) — see AGENTS.md's Status section for exactly which modules are +implemented today. This repo also holds the original architecture proposal +([`concept.md`](concept.md)). ## Scaffold Gaps @@ -45,9 +53,9 @@ workflows, placeholder modules) deliberately does **not** include yet: produces.** There's no `[tool.uv.sources]` override and no sibling-repo path dependency — the `agenticlens` extra resolves straight from PyPI. -## Design Constraints (read before building v0.1) +## Design Constraints (read before building v0.1/v0.2) -These four are additions to the original proposal, made explicit because +These five are additions to the original proposal, made explicit because they change how early modules must be built — not just what they do. 1. **One framework adapter first, not nine.** [`concept.md` @@ -228,16 +236,19 @@ The package should focus on these domains: ``` agentic-sidecar/ src/agentic_sidecar/ - core/ # Sidecar runtime, attach(), decision boundaries - sidecar.py # Sidecar class, attach() - context.py # DecisionContext passed to every evaluator + core/ # Sidecar runtime, decision boundaries -- v0.1/v0.2, implemented + sidecar.py # Sidecar class, evaluate(), before_tool_call hook + context.py # DecisionContext, IntentSnapshot, HistoryEntry decision.py # Decision(status, risk, reason, ...) - intent/ # Intent Guardian (v0.2) + operators.py # shared comparator vocabulary (gate.risk + intent.alignment) + exceptions.py # SidecarBlockedError, raised by adapters in Govern mode + intent/ # Intent Guardian -- v0.2, implemented (constraints only; + # `authority` is a field with no binding mechanism yet) envelope.py # IntentEnvelope model - alignment.py # drift detection, constraint validation - gate/ # Decision Gate (v0.1 rules, v0.4 full outcomes) - policy.py # Policy Advisor — deterministic YAML rules - risk.py # Risk Evaluator — rule-based, then pluggable + alignment.py # ConstraintBinding, IntentGuardian, drift/expiry checks + gate/ # Decision Gate (v0.1 rules -- implemented; v0.4 full outcomes) + policy.py # Policy Advisor — deterministic YAML rules -- implemented + risk.py # Risk Evaluator — rule-based, then pluggable -- implemented budget.py # Budget Guardian (v0.4) evaluators/ # Planner, Critic & Judge (v0.3) planner.py # independently evaluates the whole plan @@ -245,17 +256,25 @@ agentic-sidecar/ judge.py # model-agnostic judge interface status/ # Status Interpreter (v0.5) narrate.py - adapters/ # Framework adapters - langgraph.py # v0.1 + adapters/ # Framework adapters -- attach(sidecar, tools) + # lives per-adapter, not on Sidecar itself + # (core/ must not import adapters/) + langgraph.py # v0.1 -- implemented crewai.py # v0.6 autogen.py # v0.6 openai_agents.py # v0.6 google_adk.py # v0.6 integrations/ # Optional adapters to sibling/third-party # projects — see Cross-Project Dependencies - agenticlens.py # v0.6 - agentic_chaos.py # placeholder — interface still TBD - semantica.py # placeholder — decision/governance export + agenticlens.py # placeholder module exists now, real + # code targets v0.6 + agentic_chaos.py # placeholder module exists now -- + # interface still TBD + # semantica.py isn't created yet -- unlike the two placeholders + # above, it has no reserved pyproject.toml extra either. It's a v0.6 + # deliverable (see Cross-Project Dependencies' `semantica` entry and + # the v0.6 Build Order entry below), listed here to show its target + # location, not its current state. cli/ # CLI entry point (status stream, v0.5) ``` @@ -287,16 +306,20 @@ against a real framework. real traffic before it's given enforcement power **Deliverables:** -- [ ] `agentic_sidecar.core` — `Sidecar`, `attach()`, `Decision(status, risk, - reason)` exactly as scoped above — no richer audit/export shape yet; - that lands at v0.4 alongside the provenance/export deliverable, once - there are enough decision outcomes and an escalation flow worth - exporting -- [ ] `agentic_sidecar.gate.policy` — YAML-driven Policy Advisor -- [ ] `agentic_sidecar.gate.risk` — rule-based Risk Evaluator -- [ ] `agentic_sidecar.adapters.langgraph` -- [ ] `on_sidecar_failure` required setting, both paths tested -- [ ] README section + 1 runnable example (plain LangGraph agent, Observe mode) +- [x] `agentic_sidecar.core` — `Sidecar`, `Decision(status, risk, reason)` + exactly as scoped above — no richer audit/export shape yet; that lands + at v0.4 alongside the provenance/export deliverable, once there are + enough decision outcomes and an escalation flow worth exporting. + `attach()` itself lives per-adapter (`adapters.langgraph.attach`), not + on `Sidecar`, so `core/` has zero dependency on `adapters/` (see + AGENTS.md's Package Boundaries) — the generic `Sidecar.attach(agent)` + shape in README.md's Planned Python API is the target once a second + adapter has proven the abstraction generalizes (Design Constraint 1) +- [x] `agentic_sidecar.gate.policy` — YAML-driven Policy Advisor +- [x] `agentic_sidecar.gate.risk` — rule-based Risk Evaluator +- [x] `agentic_sidecar.adapters.langgraph` +- [x] `on_sidecar_failure` required setting, both paths tested +- [x] README section + 1 runnable example (plain LangGraph agent, Observe mode) ### v0.2 — Intent Guardian @@ -313,17 +336,39 @@ against a real framework. gated behind `on_sidecar_failure` from v0.1 **Deliverables:** -- [ ] `agentic_sidecar.intent` — `IntentEnvelope`, alignment scoring -- [ ] Lightweight `DecisionContext` snapshot type for tool call, intent, - constraints, risk factors, and execution history, implemented as a - local model -- [ ] Constraint validation against numeric/enum/allow-list fields -- [ ] Intent-drift `WARN`/`BLOCK` wired into the v0.1 Decision Gate -- [ ] `IntentEnvelope` field shapes documented against `ai-operations-spec` +- [x] `agentic_sidecar.intent` — `IntentEnvelope`, `IntentGuardian` + + `evaluate_alignment()` (alignment scoring). Scope note: `authority` + (concept.md §6's granted/not-granted flags) is a field on the + envelope but has no binding/enforcement mechanism yet — only + `constraints` does (via `ConstraintBinding`, below). Concept.md §9 + itself frames constraint validation as the *first* concrete + semantic-authorization check, not the only one; authority-based + blocking is deferred until a real scenario motivates its binding + shape, the same reasoning Design Constraint 4 applies to the risk + classifier +- [x] Lightweight `DecisionContext` snapshot type for tool call, intent, + constraints, and execution history, implemented as a local model: + `core/context.py`'s `IntentSnapshot` (goal + constraints only, not + the full `IntentEnvelope` -- see `IntentEnvelope.to_snapshot()`) and + `HistoryEntry`, auto-populated by `Sidecar.evaluate()` from its own + decision log. No separate "risk factors" field was added -- nothing + in v0.1 or v0.2 needed one distinct from what `RiskResult` already + carries +- [x] Constraint validation against numeric/enum/allow-list fields: + `intent.alignment.ConstraintBinding` binds one `constraints` entry to + a specific tool argument and comparison op (reusing `core.operators`, + the same comparator vocabulary `gate.risk.RiskRule` uses, refactored + out to avoid duplicating it) +- [x] Intent-drift `WARN`/`BLOCK` wired into the v0.1 Decision Gate: an + expired envelope produces `WARN`; a violated `ConstraintBinding` + produces `WARN` or `BLOCK` per its own `severity` +- [x] `IntentEnvelope` field shapes documented against `ai-operations-spec` conventions (informal alignment only — formal publication is v1.0, - see Cross-Project Dependencies) -- [ ] README section + example reproducing the refund-limit scenario from - [`concept.md` §22](concept.md) + see Cross-Project Dependencies) — see `intent/envelope.py`'s module + docstring +- [x] README section + example reproducing the refund-limit scenario from + [`concept.md` §9](concept.md) (not §22, which is the DEV/production + cleanup scenario used by the v0.2.x benchmark below instead) ### v0.2.x — Early Validation Benchmark (narrow) diff --git a/examples/README.md b/examples/README.md index 755bee9..2043ffe 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,7 +1,38 @@ # Examples -No runnable examples yet — `agentic-sidecar` is at the scaffold stage (see -[../ROADMAP.md](../ROADMAP.md)). The first example lands alongside v0.1 -(Sidecar Runtime + Rule-Based Decision Gate), matching the pattern used in -the other DeepAgentLabs packages: one runnable script per shipped -capability, added in the same PR as the code it demonstrates. +One runnable script per shipped capability, added in the same PR as the +code it demonstrates (see [../ROADMAP.md](../ROADMAP.md)). + +## v0.1 — Sidecar Runtime + Rule-Based Decision Gate + +[`langgraph_refund_observe_mode.py`](langgraph_refund_observe_mode.py) -- +a real `langgraph.prebuilt.create_react_agent` agent with a plain LangGraph +adapter (`agentic_sidecar.adapters.langgraph.attach`) wired up, showing the +v0.1 rule-based Decision Gate (Policy Advisor + Risk Evaluator, zero LLM +calls) evaluate two proposed tool calls in Observe mode: a refund over its +authorized amount and a destructive delete blocked by policy. Both actions +still execute -- Observe mode only logs what the Sidecar *would* have +decided; Govern mode (where `BLOCK` actually stops the call) ships in v0.2. + +Uses a small scripted chat model instead of a real LLM provider, so it runs +deterministically offline with no API key. + +```bash +uv sync --extra dev --extra langgraph +uv run python examples/langgraph_refund_observe_mode.py +``` + +## v0.2 — Intent Guardian + +[`langgraph_intent_guardian_govern_mode.py`](langgraph_intent_guardian_govern_mode.py) +-- the refund-limit scenario from concept.md §9, but in Govern mode: an +`IntentEnvelope` with a `maximum_refund: 500` constraint, bound to +`issue_refund`'s `amount` argument via a `ConstraintBinding`. A refund +request for $850 raises `SidecarBlockedError` *before* the real tool +runs -- unlike the v0.1 example, the call genuinely never executes. A +second, compliant $120 refund goes through normally. + +```bash +uv sync --extra dev --extra langgraph +uv run python examples/langgraph_intent_guardian_govern_mode.py +``` diff --git a/examples/langgraph_intent_guardian_govern_mode.py b/examples/langgraph_intent_guardian_govern_mode.py new file mode 100644 index 0000000..fbdd42c --- /dev/null +++ b/examples/langgraph_intent_guardian_govern_mode.py @@ -0,0 +1,139 @@ +"""Runnable example: Intent Guardian + Govern mode actually blocking a real +LangGraph tool call -- the refund-limit scenario from concept.md §9. + +Requires the `langgraph` extra: + + uv sync --extra langgraph + # or: pip install agentic-sidecar[langgraph] + +Run: + + uv run python examples/langgraph_intent_guardian_govern_mode.py + +Uses a small scripted chat model instead of a real LLM provider, so it runs +deterministically and offline, with no API key required. + +What it demonstrates, in contrast to `langgraph_refund_observe_mode.py` +(v0.1, Observe mode -- logs but never stops a call): + - An `IntentEnvelope` with a `maximum_refund` constraint, bound to the + `issue_refund` tool's `amount` argument via `ConstraintBinding` + - `mode="govern"`: a refund over the envelope's authorized limit raises + `SidecarBlockedError` *before* the real tool runs -- the call never + executes + - A refund within the limit still goes through normally +""" + +from __future__ import annotations + +from typing import Any + +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import AIMessage, BaseMessage +from langchain_core.outputs import ChatGeneration, ChatResult + +# create_react_agent moved to langchain.agents.create_agent in LangGraph +# 1.0+ (still functional here, just deprecated) -- kept as the import here +# so this example only needs the `langgraph` extra, not the heavier +# `langchain` package. +from langgraph.prebuilt import create_react_agent + +from agentic_sidecar import Sidecar, SidecarBlockedError +from agentic_sidecar.adapters.langgraph import attach +from agentic_sidecar.intent.alignment import ConstraintBinding, IntentGuardian +from agentic_sidecar.intent.envelope import IntentEnvelope, Requester + + +def issue_refund(order_id: str, amount: float) -> str: + """Issue a refund for an order.""" + return f"refunded ${amount:.2f} for order {order_id}" + + +class ScriptedToolCallingModel(BaseChatModel): + """Replays a fixed sequence of `AIMessage`s -- see + `langgraph_refund_observe_mode.py` for why this exists. + """ + + responses: list[AIMessage] + step: int = 0 + + def bind_tools(self, tools: Any, **kwargs: Any) -> ScriptedToolCallingModel: + return self + + def _generate( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: Any = None, + **kwargs: Any, + ) -> ChatResult: + message = self.responses[self.step] + self.step += 1 + return ChatResult(generations=[ChatGeneration(message=message)]) + + @property + def _llm_type(self) -> str: + return "scripted" + + +def refund_request(order_id: str, amount: float) -> AIMessage: + return AIMessage( + content="", + tool_calls=[ + { + "name": "issue_refund", + "args": {"order_id": order_id, "amount": amount}, + "id": "call_1", + } + ], + ) + + +def main() -> None: + # concept.md §6's worked example: a $500 refund authorization. + envelope = IntentEnvelope( + goal="refund_customer", + requested_by=Requester(type="human", id="user123"), + constraints={"maximum_refund": 500}, + ) + binding = ConstraintBinding( + constraint="maximum_refund", tool="issue_refund", arg_name="amount", op="lte" + ) + guardian = IntentGuardian(envelope, [binding]) + + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + roles=["policy", "risk", "intent_guardian"], + intent=guardian, + mode="govern", # v0.2: BLOCK is now enforced, not just logged + ) + tools = attach(sidecar, [issue_refund]) + + print("--- Attempt 1: refund of $850 (exceeds the $500 authorization) ---") + agent_over_limit = create_react_agent( + ScriptedToolCallingModel(responses=[refund_request("A100", 850.0)]), tools=tools + ) + try: + agent_over_limit.invoke({"messages": [("user", "Refund order A100 for $850.")]}) + print("Refund went through -- this shouldn't happen with an $850 request.") + except SidecarBlockedError as exc: + print(f"Blocked before the real tool ran: {exc}") + + print("\n--- Attempt 2: refund of $120 (within the $500 authorization) ---") + agent_within_limit = create_react_agent( + ScriptedToolCallingModel( + responses=[refund_request("A100", 120.0), AIMessage(content="Refund issued.")] + ), + tools=tools, + ) + result = agent_within_limit.invoke({"messages": [("user", "Refund order A100 for $120.")]}) + print(f"Final agent message: {result['messages'][-1].content}") + + print("\n--- Sidecar decision log ---") + for context, decision in sidecar.decisions: + print( + f"{decision.status:>5} {context.tool_name}({context.tool_args}) -- {decision.reason}" + ) + + +if __name__ == "__main__": + main() diff --git a/examples/langgraph_refund_observe_mode.py b/examples/langgraph_refund_observe_mode.py new file mode 100644 index 0000000..e8499da --- /dev/null +++ b/examples/langgraph_refund_observe_mode.py @@ -0,0 +1,174 @@ +"""Runnable example: a real LangGraph tool-calling agent with +`agentic-sidecar` attached via the v0.1 LangGraph adapter, in Observe mode. + +Requires the `langgraph` extra: + + uv sync --extra langgraph + # or: pip install agentic-sidecar[langgraph] + +Run: + + uv run python examples/langgraph_refund_observe_mode.py + +This script uses a small scripted chat model instead of a real LLM provider +so it runs deterministically and offline, with no API key required -- the +point of the example is the Sidecar's interception behavior, not model +quality. + +What it demonstrates: + - Policy Advisor blocking a destructive tool outright (`delete_customer_record`) + - Risk Evaluator flagging a refund that exceeds its authorized amount + - Observe mode (v0.1's scope): both proposed actions are still executed -- + the Sidecar only *logs* what it would have decided. Govern mode, where a + BLOCK actually stops the call, ships in v0.2 (see ROADMAP.md). +""" + +from __future__ import annotations + +import logging +from typing import Any + +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import AIMessage, BaseMessage +from langchain_core.outputs import ChatGeneration, ChatResult + +# create_react_agent moved to langchain.agents.create_agent in LangGraph +# 1.0+ (still functional here, just deprecated) -- kept as the import here +# so this example only needs the `langgraph` extra, not the heavier +# `langchain` package. +from langgraph.prebuilt import create_react_agent + +from agentic_sidecar import Sidecar +from agentic_sidecar.adapters.langgraph import attach +from agentic_sidecar.gate.policy import PolicyAdvisor, PolicyRule +from agentic_sidecar.gate.risk import RiskEvaluator, RiskRule + +logging.basicConfig(level=logging.INFO, format="%(message)s") + + +# --- Tools the agent can call ------------------------------------------------ + + +def look_up_order(order_id: str) -> str: + """Look up an order's status and total.""" + return f"order {order_id}: status=delivered, total=$120.00" + + +def issue_refund(order_id: str, amount: float) -> str: + """Issue a refund for an order.""" + return f"refunded ${amount:.2f} for order {order_id}" + + +def delete_customer_record(customer_id: str) -> str: + """Permanently delete a customer's record.""" + return f"deleted customer {customer_id}" + + +# --- A scripted chat model, so this example runs with no API key ------------ + + +class ScriptedToolCallingModel(BaseChatModel): + """Replays a fixed sequence of `AIMessage`s instead of calling a real LLM + provider -- just enough surface for `create_react_agent` to drive a real + tool-calling loop deterministically and offline. + """ + + responses: list[AIMessage] + step: int = 0 + + def bind_tools(self, tools: Any, **kwargs: Any) -> ScriptedToolCallingModel: + return self + + def _generate( + self, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: Any = None, + **kwargs: Any, + ) -> ChatResult: + message = self.responses[self.step] + self.step += 1 + return ChatResult(generations=[ChatGeneration(message=message)]) + + @property + def _llm_type(self) -> str: + return "scripted" + + +def main() -> None: + # v0.1 Decision Gate: deterministic rules, zero LLM calls (ROADMAP.md's + # Design Constraint 2). These could just as easily come from + # PolicyAdvisor.from_yaml("policies.yaml") / RiskEvaluator.from_yaml(...). + policy = PolicyAdvisor( + [ + PolicyRule( + tool="delete_*", + effect="deny", + reason="Destructive operations are never permitted by policy", + ) + ] + ) + risk = RiskEvaluator( + [ + RiskRule( + tool="issue_refund", + arg_name="amount", + arg_op="gt", + arg_value=500, + risk="HIGH", + reason="Refund exceeds the standard $500 authorization limit", + ) + ] + ) + + # fail_closed: if the Sidecar itself errors, block rather than silently + # let an unevaluated action through (ROADMAP.md's Design Constraint 3). + sidecar = Sidecar(on_sidecar_failure="fail_closed", policy=policy, risk=risk) + + tools = attach(sidecar, [look_up_order, issue_refund, delete_customer_record]) + + model = ScriptedToolCallingModel( + responses=[ + AIMessage( + content="", + tool_calls=[ + { + "name": "issue_refund", + "args": {"order_id": "A100", "amount": 850.0}, + "id": "call_1", + } + ], + ), + AIMessage( + content="", + tool_calls=[ + { + "name": "delete_customer_record", + "args": {"customer_id": "C42"}, + "id": "call_2", + } + ], + ), + AIMessage(content="Done: refunded order A100 and removed customer C42's record."), + ] + ) + agent = create_react_agent(model, tools=tools) + + print("--- Running agent (Observe mode: Sidecar logs decisions, never blocks) ---\n") + result = agent.invoke( + {"messages": [("user", "Refund order A100 for $850 and delete customer C42's record.")]} + ) + + print("\n--- Final agent message ---") + print(result["messages"][-1].content) + + print("\n--- Sidecar decision log (what Govern mode, v0.2+, would enforce) ---") + for context, decision in sidecar.decisions: + print( + f"{decision.status:>5} risk={decision.risk!s:<6} " + f"{context.tool_name}({context.tool_args}) -- {decision.reason}" + ) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 4ceb735..10bea8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentic-sidecar" -version = "0.0.1" +version = "0.2.0" description = "Companion intelligence and real-time decision supervision for autonomous AI agents." readme = "README.md" license = { file = "LICENSE" } @@ -25,6 +25,8 @@ classifiers = [ # once the CLI (v0.5, see ROADMAP.md) has real code -- not before. dependencies = [ "pydantic>=2.0,<3", + # gate.policy / gate.risk load their rule sets from YAML (v0.1). + "pyyaml>=6.0,<7", ] [project.optional-dependencies] @@ -42,6 +44,13 @@ agenticlens = [ agentic-chaos = [ "agentic-chaos>=0.1", ] +# Only needed to run the LangGraph example in examples/ or to build a real +# graph around agentic_sidecar.adapters.langgraph.attach()'s wrapped tools -- +# the adapter module itself has no import-time dependency on `langgraph` +# (see src/agentic_sidecar/adapters/langgraph.py). +langgraph = [ + "langgraph>=1.0,<2", +] dev = [ "pytest>=8.0", "pytest-cov>=5.0", @@ -49,6 +58,7 @@ dev = [ "mypy>=1.10", "build>=1.2", "twine>=5.1", + "types-PyYAML>=6.0", ] # [project.scripts] intentionally omitted -- add diff --git a/src/agentic_sidecar/__init__.py b/src/agentic_sidecar/__init__.py index 725ba29..40adf05 100644 --- a/src/agentic_sidecar/__init__.py +++ b/src/agentic_sidecar/__init__.py @@ -3,13 +3,26 @@ The Main Agent acts. The Sidecar observes, thinks, advises, and governs. -This package is currently a **scaffold** -- directory layout, tooling, and -CI/release workflows exist, but nothing under `agentic_sidecar` is -implemented yet. See ROADMAP.md for the build order, starting with a -rule-based, LLM-free v0.1 Decision Gate, and README.md for the architecture -and planned Python API. +v0.1 shipped the Sidecar runtime and a rule-based, LLM-free Decision Gate +(Policy Advisor + Risk Evaluator), attached via the LangGraph adapter, in +Observe mode. v0.2 adds Intent Guardian (`agentic_sidecar.intent`) and +Govern mode, where a `BLOCK` decision is actually enforced. See ROADMAP.md +for the full build order and README.md for the architecture and Python API. """ -__version__ = "0.0.1" +from agentic_sidecar.core.context import DecisionContext +from agentic_sidecar.core.decision import Decision, DecisionStatus, RiskLevel +from agentic_sidecar.core.exceptions import SidecarBlockedError +from agentic_sidecar.core.sidecar import Sidecar -__all__ = ["__version__"] +__version__ = "0.2.0" + +__all__ = [ + "Decision", + "DecisionContext", + "DecisionStatus", + "RiskLevel", + "Sidecar", + "SidecarBlockedError", + "__version__", +] diff --git a/src/agentic_sidecar/adapters/__init__.py b/src/agentic_sidecar/adapters/__init__.py index 48e3615..d6522b5 100644 --- a/src/agentic_sidecar/adapters/__init__.py +++ b/src/agentic_sidecar/adapters/__init__.py @@ -1,8 +1,16 @@ -"""Framework adapters -- the `before_tool_call` / decision-boundary -interception point for each supported agent framework. +"""Framework adapters -- the decision-boundary interception point for each +supported agent framework, routing proposed actions through +`Sidecar.evaluate()`. -`langgraph.py` is the only adapter planned for v0.1. Do not start a second -adapter before it is done and stable -- the interception abstraction needs -to survive contact with one real framework first (ROADMAP.md's Design -Constraint 1). The other four are placeholders for v0.6. +`langgraph.py` is implemented as of v0.1 and is the only adapter that should +exist until it's proven stable -- do not start a second adapter yet, the +interception abstraction needs to survive contact with one real framework +first (ROADMAP.md's Design Constraint 1). The other four +(`crewai`, `autogen`, `openai_agents`, `google_adk`) are placeholders for +v0.6. + +No module in this package is imported here automatically: each adapter is a +separate optional surface (see `pyproject.toml`'s `langgraph` extra), and +importing `agentic_sidecar.adapters` should never require every framework's +SDK to be installed. """ diff --git a/src/agentic_sidecar/adapters/langgraph.py b/src/agentic_sidecar/adapters/langgraph.py index 379abcb..3e6a149 100644 --- a/src/agentic_sidecar/adapters/langgraph.py +++ b/src/agentic_sidecar/adapters/langgraph.py @@ -1,7 +1,81 @@ """LangGraph interception adapter -- the first and, until it's proven out, -only decision-boundary hook. LangGraph was chosen as the starting framework -because its explicit graph/node structure gives `before_tool_call`-style -interception the clearest place to attach. +only decision-boundary hook (ROADMAP.md's Design Constraint 1). -Planned for v0.1 -- see ROADMAP.md. Not implemented yet. +`attach()` wraps a list of tool callables -- the same list you'd otherwise +hand to `langgraph.prebuilt.create_react_agent(model, tools=...)` -- so that +every invocation is evaluated by a `Sidecar` first. This module has no +import-time dependency on the `langgraph` package itself: it only relies on +LangGraph/LangChain's convention that a tool is a plain callable with a +stable `__name__` and a type-annotated signature, which is exactly what +`create_react_agent` accepts without an explicit `@tool` decorator. Install +`agentic-sidecar[langgraph]` to actually build and run a graph around the +wrapped tools (see `examples/`). + +In `sidecar.mode="observe"` (v0.1's only mode, still the default), the +wrapped tool always calls through to the real tool regardless of +`Decision.status` -- the Sidecar's verdict is only computed and recorded +(`sidecar.decisions`). In `mode="govern"` (v0.2+), a `BLOCK` verdict raises +`SidecarBlockedError` instead of calling the real tool; `ALLOW` and `WARN` both +still call through -- `WARN` is advisory, not a stop signal (see README.md's +Operating modes). """ + +from __future__ import annotations + +import functools +import inspect +from collections.abc import Callable, Sequence +from typing import Any, TypeVar + +from agentic_sidecar.core.context import DecisionContext +from agentic_sidecar.core.exceptions import SidecarBlockedError +from agentic_sidecar.core.sidecar import Sidecar + +F = TypeVar("F", bound=Callable[..., Any]) + + +def attach(sidecar: Sidecar, tools: Sequence[F]) -> list[F]: + """Wrap each tool in `tools` so calling it first runs `sidecar.evaluate()`. + + Returns a new list in the same order -- pass it straight to + `create_react_agent(model, tools=attach(sidecar, tools))`. Does not + mutate the input list or the original tool callables. + """ + return [_wrap_tool(sidecar, tool) for tool in tools] + + +def _wrap_tool(sidecar: Sidecar, tool: F) -> F: + signature = inspect.signature(tool) + tool_name = getattr(tool, "__name__", repr(tool)) + + @functools.wraps(tool) + def _sidecar_wrapped_tool(*args: Any, **kwargs: Any) -> Any: + context = DecisionContext( + tool_name=tool_name, + tool_args=_bind_args(signature, args, kwargs), + ) + decision = sidecar.evaluate(context) + # Enforcement only exists in Govern mode -- Observe mode (v0.1's + # only mode, still the default) always calls through regardless of + # `decision.status`. `WARN` never stops the call in either mode. + if sidecar.mode == "govern" and decision.status == "BLOCK": + raise SidecarBlockedError(decision, context) + return tool(*args, **kwargs) + + return _sidecar_wrapped_tool # type: ignore[return-value] + + +def _bind_args( + signature: inspect.Signature, args: tuple[Any, ...], kwargs: dict[str, Any] +) -> dict[str, Any]: + try: + bound = signature.bind(*args, **kwargs) + except TypeError: + # A call that doesn't match the tool's declared signature shouldn't + # crash the Decision Gate -- fall back to raw kwargs (positional + # args are lost in this case, but LangGraph's ToolNode always calls + # tools with keyword arguments derived from the model's structured + # tool call, so this path is a defensive fallback, not the norm). + return dict(kwargs) + bound.apply_defaults() + return dict(bound.arguments) diff --git a/src/agentic_sidecar/core/__init__.py b/src/agentic_sidecar/core/__init__.py index 3b24ef7..bb97af6 100644 --- a/src/agentic_sidecar/core/__init__.py +++ b/src/agentic_sidecar/core/__init__.py @@ -1,5 +1,16 @@ -"""Sidecar runtime: attach() an existing agent, intercept decision -boundaries, and produce Decision objects. +"""Sidecar runtime: `Sidecar`, `Decision`, `DecisionContext`, the shared +comparator vocabulary (`operators.py`), and `SidecarBlockedError` +(`exceptions.py`) -- the primitives every adapter +(`agentic_sidecar.adapters`) and Decision Gate module +(`agentic_sidecar.gate`, `agentic_sidecar.intent`) is built on. -Planned for v0.1 -- see ROADMAP.md. Not implemented yet. +Implemented as of v0.1 (Sidecar, Decision, DecisionContext) and v0.2 +(Govern mode, `operators.py`, `exceptions.py`) -- see ROADMAP.md. `core/` +must not import from `adapters/` (AGENTS.md's Package Boundaries): adapters +route a specific framework's decision boundaries through +`Sidecar.evaluate()`, not the other way around. `core/` *does* import from +`gate/` and `intent/` directly (Sidecar wires in Policy Advisor, Risk +Evaluator, and Intent Guardian as built-in modules), which is the expected +direction -- those are Decision Gate modules Sidecar orchestrates, not +alternate entry points the way adapters are. """ diff --git a/src/agentic_sidecar/core/context.py b/src/agentic_sidecar/core/context.py index c4ccc00..71d9acd 100644 --- a/src/agentic_sidecar/core/context.py +++ b/src/agentic_sidecar/core/context.py @@ -1,6 +1,67 @@ """`DecisionContext` -- what gets passed to every evaluator (Policy, Risk, -Intent, Critic, Judge) at a decision boundary: the proposed tool call, the -active IntentEnvelope, accumulated execution history, and risk metadata. +and, from v0.2, Intent Guardian) at a decision boundary. -Planned for v0.1 -- see ROADMAP.md. Not implemented yet. +`DecisionContext` itself carries only a lightweight `IntentSnapshot`, not +the full `IntentEnvelope` (that lives in `agentic_sidecar.intent.envelope`, +which `core/` does not depend on -- see `IntentEnvelope.to_snapshot()`). +This matches concept.md §7, Intent Injection: only the relevant intent +needs to reach each decision boundary, not the whole envelope. `metadata` +remains the escape hatch for anything a custom `@sidecar.before_tool_call` +hook or future evaluator wants to attach without a breaking change here. """ + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, Field + +from agentic_sidecar.core.decision import DecisionStatus + + +class IntentSnapshot(BaseModel): + """A lightweight, decision-time view of the active `IntentEnvelope` -- + goal and constraints only. `Sidecar.evaluate()` builds this from + `IntentEnvelope.to_snapshot()`; it is not something callers construct + directly. + """ + + goal: str + constraints: dict[str, Any] = Field(default_factory=dict) + + +class HistoryEntry(BaseModel): + """One prior decision in this Sidecar's evaluation history. + `Sidecar.evaluate()` populates `DecisionContext.history` from its own + `self.decisions` log before dispatching to an evaluator -- also not + something callers build directly. + """ + + tool_name: str + tool_args: dict[str, Any] = Field(default_factory=dict) + status: DecisionStatus + + +class DecisionContext(BaseModel): + """The proposed action being evaluated at a decision boundary. + + `tool_name` and `tool_args` describe the tool call itself (see + concept.md §14's decision boundaries -- v0.1 only intercepts tool + invocation). `intent` and `history` are injected by `Sidecar.evaluate()` + itself, not by the caller constructing the context -- an adapter (e.g. + `agentic_sidecar.adapters.langgraph`) only needs to supply `tool_name` + and `tool_args`. + + Deliberately *not* frozen (unlike `Decision`): `Sidecar.evaluate()` + injects `intent`/`history` by mutating the instance the caller passed + in, in place, so that caller's own reference reflects the fully + evaluated context once `evaluate()` returns -- e.g. an adapter building + `SidecarBlockedError` after the call gets the context Intent Guardian + actually saw, not the bare one it originally constructed. + """ + + tool_name: str + tool_args: dict[str, Any] = Field(default_factory=dict) + intent: IntentSnapshot | None = None + history: list[HistoryEntry] = Field(default_factory=list) + metadata: dict[str, Any] = Field(default_factory=dict) diff --git a/src/agentic_sidecar/core/decision.py b/src/agentic_sidecar/core/decision.py index 3c6a31c..f0b9acc 100644 --- a/src/agentic_sidecar/core/decision.py +++ b/src/agentic_sidecar/core/decision.py @@ -1,9 +1,43 @@ -"""`Decision(status, risk, reason, ...)` -- the Decision Gate's output type. +"""`Decision(status, risk, reason)` -- the Decision Gate's output type. -v0.1 ships `status in {"ALLOW", "BLOCK"}` only. The full outcome set -(`ALLOW`, `WARN`, `REPLAN`, `PAUSE`, `BLOCK`, `ESCALATE`) lands in v0.4 -- -see README.md's "How the Decision Gate evaluates a decision" and -ROADMAP.md's build order. +v0.1 shipped `status in {"ALLOW", "BLOCK"}` only. v0.2 adds `WARN` -- +Intent Guardian's outcome for a finding that's worth surfacing (e.g. a +stale/expired envelope) but not severe enough to block. `CHALLENGE`, +`REPLAN`, `PAUSE`, and `ESCALATE` -- the rest of concept.md §15's seven +outcomes -- land at v0.4. See README.md's "How the Decision Gate evaluates +a decision" and ROADMAP.md's build order. +""" + +from __future__ import annotations + +from typing import Literal -Planned for v0.1 -- see ROADMAP.md. Not implemented yet. +from pydantic import BaseModel, ConfigDict + +DecisionStatus = Literal["ALLOW", "WARN", "BLOCK"] +"""v0.1 shipped ALLOW/BLOCK; v0.2 adds WARN. The remaining outcomes from +concept.md §15's seven land at v0.4. """ + +RiskLevel = Literal["LOW", "MEDIUM", "HIGH"] +"""Risk Evaluator classification (gate/risk.py) -- static rules only in v0.1.""" + + +class Decision(BaseModel): + """The outcome of a single Decision Gate evaluation for one proposed + action (tool call). + + `risk` is `None` only on the `on_sidecar_failure` path (core/sidecar.py) + -- when the Sidecar itself errored before Policy/Risk ever ran, there is + no risk classification to report, and reporting one would misrepresent + what actually happened. + + Frozen: a `Decision` is a record of what was decided, not a value meant + to be mutated after the fact. + """ + + model_config = ConfigDict(frozen=True) + + status: DecisionStatus + risk: RiskLevel | None + reason: str diff --git a/src/agentic_sidecar/core/exceptions.py b/src/agentic_sidecar/core/exceptions.py new file mode 100644 index 0000000..a8b7a67 --- /dev/null +++ b/src/agentic_sidecar/core/exceptions.py @@ -0,0 +1,30 @@ +"""Exceptions raised by adapters when a Sidecar's Decision Gate blocks a +proposed action in Govern mode (v0.2+). + +Not raised by `Sidecar.evaluate()` itself -- a `BLOCK` `Decision` is a +normal outcome, not a Python exception. Turning it into a raised exception +(and thereby actually stopping the call) is each adapter's job at its own +interception point, not core's (AGENTS.md's Package Boundaries: adapters +depend on core, not the reverse -- `core/` has no opinion on what "stopping +a call" means for a given framework). +""" + +from __future__ import annotations + +from agentic_sidecar.core.context import DecisionContext +from agentic_sidecar.core.decision import Decision + + +class SidecarBlockedError(RuntimeError): + """Raised by an adapter (e.g. `agentic_sidecar.adapters.langgraph`) in + Govern mode when the Decision Gate returns `BLOCK` for a proposed tool + call, instead of letting the call through. Never raised in Observe + mode (v0.1's only mode) -- see ROADMAP.md's v0.1/v0.2 scope. + """ + + def __init__(self, decision: Decision, context: DecisionContext) -> None: + self.decision = decision + self.context = context + super().__init__( + f"Sidecar blocked '{context.tool_name}'({context.tool_args}): {decision.reason}" + ) diff --git a/src/agentic_sidecar/core/operators.py b/src/agentic_sidecar/core/operators.py new file mode 100644 index 0000000..ebf6a34 --- /dev/null +++ b/src/agentic_sidecar/core/operators.py @@ -0,0 +1,45 @@ +"""Shared comparison-operator vocabulary. + +Used by both `gate/risk.py`'s argument-pattern rules (v0.1) and +`intent/alignment.py`'s constraint bindings (v0.2) so the two don't +duplicate identical comparator logic -- both ultimately answer the same +question ("does this tool argument satisfy this comparison against this +value?"), just for different callers. +""" + +from __future__ import annotations + +from typing import Any, Literal + +ArgOp = Literal["eq", "ne", "gt", "gte", "lt", "lte", "in", "contains"] + + +def compare(actual: Any, op: ArgOp, expected: Any) -> bool: + """Evaluate `actual expected`. + + Never raises: a type mismatch (e.g. comparing a string argument with + `gt`) returns `False` rather than propagating a `TypeError` -- a + malformed rule or an unexpectedly-typed argument shouldn't crash the + Decision Gate (see core/sidecar.py's `on_sidecar_failure` for the + analogous handling one layer up). + """ + try: + if op == "eq": + return bool(actual == expected) + if op == "ne": + return bool(actual != expected) + if op == "gt": + return bool(actual > expected) + if op == "gte": + return bool(actual >= expected) + if op == "lt": + return bool(actual < expected) + if op == "lte": + return bool(actual <= expected) + if op == "in": + return bool(actual in expected) + if op == "contains": + return bool(expected in actual) + except TypeError: + return False + return False diff --git a/src/agentic_sidecar/core/sidecar.py b/src/agentic_sidecar/core/sidecar.py index c278ba6..5e1e50b 100644 --- a/src/agentic_sidecar/core/sidecar.py +++ b/src/agentic_sidecar/core/sidecar.py @@ -1,9 +1,290 @@ -"""The `Sidecar` class -- `Sidecar(roles=[...])` and `sidecar.attach(agent)`. +"""The `Sidecar` class -- owns module configuration (which of Policy/Risk/ +Intent/Critic/Judge/Budget are enabled) and the required `on_sidecar_failure` +setting. -Owns module configuration (which of Policy/Risk/Intent/Critic/Judge/Budget -are enabled) and the required `on_sidecar_failure` setting (`fail_open` / -`fail_closed`, no default -- see README.md and ROADMAP.md's Design -Constraints). - -Planned for v0.1 -- see ROADMAP.md. Not implemented yet. +v0.1 wired up Policy Advisor + Risk Evaluator, in Observe mode. v0.2 adds +Intent Guardian and Govern mode: the Decision Gate now computes a `WARN` +outcome (intent-drift findings not severe enough to block) alongside +`ALLOW`/`BLOCK`, and `mode="govern"` is available for an adapter to +actually enforce a `BLOCK` rather than only logging it (see +`agentic_sidecar.adapters.langgraph`). `on_sidecar_failure: +fail_open | fail_closed` has no default: every Decision Gate evaluation +must resolve even when the Sidecar itself errors (ROADMAP.md's Design +Constraint 3). """ + +from __future__ import annotations + +import logging +from collections.abc import Callable, Sequence +from typing import Literal + +from agentic_sidecar.core.context import DecisionContext, HistoryEntry +from agentic_sidecar.core.decision import Decision, DecisionStatus, RiskLevel +from agentic_sidecar.gate.policy import PolicyAdvisor +from agentic_sidecar.gate.risk import RISK_ORDER, RiskEvaluator +from agentic_sidecar.intent.alignment import AlignmentResult, IntentGuardian + +logger = logging.getLogger("agentic_sidecar") + +SidecarFailureMode = Literal["fail_open", "fail_closed"] +SidecarMode = Literal["observe", "govern"] +DecisionHook = Callable[[DecisionContext], Decision] + +_SUPPORTED_ROLES = frozenset({"policy", "risk", "intent_guardian"}) +_KNOWN_FUTURE_ROLES: dict[str, str] = { + "planner": "v0.3", + "critic": "v0.3", + "judge": "v0.3", + "budget": "v0.4", +} + + +class Sidecar: + """Owns the Decision Gate: Policy Advisor, Risk Evaluator, and (v0.2) + Intent Guardian, combined into a single `Decision` per proposed action. + + `on_sidecar_failure` is a required keyword argument with no default -- + pick `"fail_closed"` unless you have a specific reason to fail open (see + ROADMAP.md's Design Constraint 3: anything that reaches a Decision Gate + at all is, by definition, a decision boundary worth being conservative + about). + + `mode="observe"` (the default) logs every `Decision` but enforces + nothing. `mode="govern"` makes a `BLOCK` real -- but only once an + adapter acts on it (`core/` computes decisions, it doesn't stop calls; + see `agentic_sidecar.core.exceptions.SidecarBlockedError` and + `agentic_sidecar.adapters.langgraph`). + + v0.1 has no `attach()` method here: routing a specific framework's + tool-call surface through `evaluate()` is an adapter's job, not core's + (AGENTS.md's Package Boundaries -- `core/` must not import from + `adapters/`). See `agentic_sidecar.adapters.langgraph.attach()`. + """ + + def __init__( + self, + *, + on_sidecar_failure: SidecarFailureMode, + policy: PolicyAdvisor | None = None, + risk: RiskEvaluator | None = None, + intent: IntentGuardian | None = None, + risk_block_threshold: RiskLevel = "HIGH", + mode: SidecarMode = "observe", + roles: Sequence[str] | None = None, + ) -> None: + if on_sidecar_failure not in ("fail_open", "fail_closed"): + raise ValueError( + f"on_sidecar_failure={on_sidecar_failure!r} is invalid -- it " + "is a required setting with no default and must be " + "'fail_open' or 'fail_closed' (ROADMAP.md's Design " + "Constraint 3). 'fail_closed' is the recommended default." + ) + if mode not in ("observe", "govern"): + raise ValueError( + f"mode={mode!r} is invalid -- must be 'observe' (logs " + "decisions, enforces nothing) or 'govern' (a BLOCK is " + "actually enforced by the attached adapter). See " + "ROADMAP.md's Operating modes." + ) + if risk_block_threshold not in RISK_ORDER: + raise ValueError( + f"risk_block_threshold={risk_block_threshold!r} is invalid " + f"-- must be one of {list(RISK_ORDER)!r}. An unrecognized " + "threshold would otherwise only surface as a KeyError " + "inside evaluate(), silently resolved via " + "on_sidecar_failure instead of failing fast here." + ) + + self.on_sidecar_failure = on_sidecar_failure + self.policy = policy or PolicyAdvisor() + self.risk = risk or RiskEvaluator() + self.risk_block_threshold = risk_block_threshold + self.mode = mode + self.roles = self._validate_roles(roles) + self.decisions: list[tuple[DecisionContext, Decision]] = [] + self._hook: DecisionHook | None = None + self.intent: IntentGuardian | None = None + self.set_intent(intent) + + @staticmethod + def _validate_roles(roles: Sequence[str] | None) -> tuple[str, ...]: + resolved = tuple(roles) if roles is not None else ("policy", "risk") + for role in resolved: + if role in _SUPPORTED_ROLES: + continue + if role in _KNOWN_FUTURE_ROLES: + raise NotImplementedError( + f"role={role!r} is not implemented until " + f"{_KNOWN_FUTURE_ROLES[role]} -- see ROADMAP.md's build " + f"order. Currently supported: {sorted(_SUPPORTED_ROLES)}." + ) + raise ValueError( + f"Unknown role {role!r}. Currently supported: {sorted(_SUPPORTED_ROLES)}." + ) + return resolved + + def set_intent(self, intent: IntentGuardian | None) -> None: + """Set (or clear) the active `IntentGuardian` -- separate from + `__init__` because a long-lived Sidecar is expected to serve one + `IntentEnvelope` per task, not one for its whole lifetime (concept.md + §6: the envelope is created "when a user initiates a task"). + + Raises if `intent` is given but `"intent_guardian"` isn't in + `self.roles` -- a `Sidecar` that would silently never consult the + guardian you just handed it is exactly the kind of silent gap + ROADMAP.md's Design Constraint 3 warns against for + `on_sidecar_failure`, and the same principle applies here. The + reverse (role enabled, no envelope yet) is *not* an error: a + multi-task Sidecar may legitimately start before its first + envelope arrives. + """ + if intent is not None and "intent_guardian" not in self.roles: + raise ValueError( + "intent=... was given but 'intent_guardian' is not in " + f"roles={list(self.roles)!r} -- it would never be " + "consulted. Add 'intent_guardian' to roles, or don't pass " + "intent." + ) + self.intent = intent + + def before_tool_call(self, func: DecisionHook) -> DecisionHook: + """Register a custom decision hook, overriding the default + Policy + Risk + Intent Guardian evaluation for every subsequent + `evaluate()` call. + + ```python + @sidecar.before_tool_call + def evaluate_action(context: DecisionContext) -> Decision: + if context.tool_name == "delete_database": + return Decision(status="BLOCK", risk="HIGH", reason="never") + return Decision(status="ALLOW", risk="LOW", reason="ok") + ``` + + Mirrors the shape sketched in README.md's Planned Python API. Once + registered, the hook fully replaces the built-in Decision Gate + modules for every `evaluate()` call -- there is no v0.2 mechanism + to run both a custom hook and the built-in modules together. The + hook still receives a context with `intent`/`history` already + injected (see `evaluate()`). + """ + self._hook = func + return func + + def evaluate(self, context: DecisionContext) -> Decision: + """Evaluate one proposed action and return a `Decision`. + + Injects the active intent snapshot and this Sidecar's decision + history into `context` first (concept.md §7, Intent Injection) -- + the caller only needs to supply `tool_name`/`tool_args`. This + mutates the `context` object the caller passed in, in place (rather + than evaluating a copy) -- `DecisionContext` is deliberately not + frozen for exactly this reason, and it means a caller (e.g. an + adapter constructing `SidecarBlockedError`) sees the fully-injected + context on its own reference after `evaluate()` returns, not the + bare one it originally built. Always resolves, per + `on_sidecar_failure`, even if the registered hook (or the default + evaluation) raises. Every call is recorded in `self.decisions` + regardless of outcome. Never raises for a `BLOCK` decision itself, + in either mode -- that a `BLOCK` should actually stop the call is + enforced by the attached adapter in Govern mode, not by + `evaluate()` (see `agentic_sidecar.core.exceptions.SidecarBlockedError`). + """ + self._inject(context) + try: + evaluator = self._hook or self._default_evaluate + decision = evaluator(context) + except Exception as exc: # noqa: BLE001 -- deliberate, see Design Constraint 3 + decision = self._on_failure(context, exc) + self._log_decision(context, decision) + self.decisions.append((context, decision)) + return decision + + def _inject(self, context: DecisionContext) -> None: + history = [ + HistoryEntry(tool_name=c.tool_name, tool_args=c.tool_args, status=d.status) + for c, d in self.decisions + ] + intent_snapshot = self.intent.envelope.to_snapshot() if self.intent is not None else None + context.intent = intent_snapshot + context.history = history + + def _default_evaluate(self, context: DecisionContext) -> Decision: + policy_result = None + if "policy" in self.roles: + policy_result = self.policy.evaluate(context) + if policy_result.effect == "deny": + return Decision( + status="BLOCK", + risk=None, + reason=f"Policy Advisor: {policy_result.reason}", + ) + + risk_result = None + if "risk" in self.roles: + risk_result = self.risk.evaluate(context) + if RISK_ORDER[risk_result.risk] >= RISK_ORDER[self.risk_block_threshold]: + return Decision( + status="BLOCK", + risk=risk_result.risk, + reason=( + f"Risk Evaluator: {risk_result.reason} " + f"(risk >= block threshold '{self.risk_block_threshold}')" + ), + ) + + alignment_result: AlignmentResult | None = None + if "intent_guardian" in self.roles and self.intent is not None: + alignment_result = self.intent.evaluate(context) + if alignment_result.status == "BLOCK": + return Decision( + status="BLOCK", + risk=risk_result.risk if risk_result else None, + reason=f"Intent Guardian: {alignment_result.reason}", + ) + + reasons = [ + f"Policy Advisor: {policy_result.reason}" if policy_result else None, + f"Risk Evaluator: {risk_result.reason}" if risk_result else None, + f"Intent Guardian: {alignment_result.reason}" if alignment_result else None, + ] + reason = "; ".join(r for r in reasons if r) or ( + "No Decision Gate modules enabled in `roles`; defaulting to ALLOW." + ) + status: DecisionStatus = ( + "WARN" if alignment_result and alignment_result.status == "WARN" else "ALLOW" + ) + return Decision( + status=status, + risk=risk_result.risk if risk_result else None, + reason=reason, + ) + + def _on_failure(self, context: DecisionContext, exc: Exception) -> Decision: + status: Literal["ALLOW", "BLOCK"] = ( + "ALLOW" if self.on_sidecar_failure == "fail_open" else "BLOCK" + ) + logger.error( + "Sidecar evaluation failed for tool '%s': %s", context.tool_name, exc, exc_info=exc + ) + return Decision( + status=status, + risk=None, + reason=( + f"Sidecar evaluation raised {exc.__class__.__name__}: {exc}. " + f"Resolved via on_sidecar_failure='{self.on_sidecar_failure}'." + ), + ) + + def _log_decision(self, context: DecisionContext, decision: Decision) -> None: + level = logging.WARNING if decision.status != "ALLOW" else logging.INFO + logger.log( + level, + "[%s] %s tool=%s args=%s risk=%s reason=%s", + self.mode.upper(), + decision.status, + context.tool_name, + context.tool_args, + decision.risk, + decision.reason, + ) diff --git a/src/agentic_sidecar/gate/__init__.py b/src/agentic_sidecar/gate/__init__.py index d1017e9..5bda78f 100644 --- a/src/agentic_sidecar/gate/__init__.py +++ b/src/agentic_sidecar/gate/__init__.py @@ -1,4 +1,5 @@ -"""The Decision Gate: Policy Advisor, Risk Evaluator, and Budget Guardian. +"""The Decision Gate: Policy Advisor, Risk Evaluator, and (v0.4) Budget +Guardian. Answers "are you permitted to do this?" (policy.py) and "how dangerous is this action?" (risk.py) -- deliberately separate from `intent/`, which @@ -7,6 +8,20 @@ and ROADMAP.md's Design Constraint 5 before adding a check here that's really an intent question. -`policy.py` and `risk.py` are planned for v0.1 (rule-based, zero LLM calls). -`budget.py` is planned for v0.4. Not implemented yet. +`policy.py` and `risk.py` are implemented as of v0.1 (rule-based, zero LLM +calls). `budget.py` is planned for v0.4 and not implemented yet. """ + +from agentic_sidecar.gate.policy import PolicyAdvisor, PolicyEffect, PolicyResult, PolicyRule +from agentic_sidecar.gate.risk import RISK_ORDER, RiskEvaluator, RiskResult, RiskRule + +__all__ = [ + "RISK_ORDER", + "PolicyAdvisor", + "PolicyEffect", + "PolicyResult", + "PolicyRule", + "RiskEvaluator", + "RiskResult", + "RiskRule", +] diff --git a/src/agentic_sidecar/gate/policy.py b/src/agentic_sidecar/gate/policy.py index 336e3e9..836cef4 100644 --- a/src/agentic_sidecar/gate/policy.py +++ b/src/agentic_sidecar/gate/policy.py @@ -2,7 +2,97 @@ Answers "are you permitted to do this?" only. No LLM calls, no intent awareness -- that's `intent/` and `evaluators/`, deliberately kept separate -(see ROADMAP.md's Design Constraint 5). - -Planned for v0.1 -- see ROADMAP.md. Not implemented yet. +(see ROADMAP.md's Design Constraint 5 and AGENTS.md's Package Boundaries: +`gate/` must work with zero dependency on `evaluators/`). """ + +from __future__ import annotations + +import fnmatch +from pathlib import Path +from typing import Any, Literal + +import yaml +from pydantic import BaseModel + +from agentic_sidecar.core.context import DecisionContext + +PolicyEffect = Literal["allow", "deny"] + + +class PolicyRule(BaseModel): + """One allow/deny rule. `tool` is an `fnmatch` glob matched against + `DecisionContext.tool_name` (e.g. `"delete_*"`, `"*"`). Rules are + evaluated in order; the first match wins. + """ + + tool: str + effect: PolicyEffect + reason: str | None = None + + +class PolicyResult(BaseModel): + """What the Policy Advisor decided for one `DecisionContext`, and why.""" + + effect: PolicyEffect + reason: str + matched_rule: PolicyRule | None = None + + +class PolicyAdvisor: + """Deterministic allow/deny rule set. + + With no rules configured, every action resolves to `default_effect` + (`"allow"` by default) -- an empty Policy Advisor is a no-op, not an + implicit deny-all. Use `from_yaml` / `from_mapping` to load a real rule + set, e.g.: + + ```yaml + default: allow + rules: + - tool: "delete_*" + effect: deny + reason: "Destructive operations require Risk Evaluator + human sign-off" + - tool: "read_*" + effect: allow + ``` + """ + + def __init__( + self, + rules: list[PolicyRule] | None = None, + *, + default_effect: PolicyEffect = "allow", + ) -> None: + self.rules = rules or [] + self.default_effect = default_effect + + @classmethod + def from_yaml(cls, path: str | Path) -> PolicyAdvisor: + """Load a rule set from a YAML file (see class docstring for shape).""" + data = yaml.safe_load(Path(path).read_text()) + return cls.from_mapping(data or {}) + + @classmethod + def from_mapping(cls, data: dict[str, Any]) -> PolicyAdvisor: + """Load a rule set from an already-parsed mapping (e.g. for tests + that don't want to touch the filesystem). + """ + rules = [PolicyRule.model_validate(rule) for rule in data.get("rules", [])] + default_effect = data.get("default", "allow") + return cls(rules, default_effect=default_effect) + + def evaluate(self, context: DecisionContext) -> PolicyResult: + for rule in self.rules: + if fnmatch.fnmatch(context.tool_name, rule.tool): + reason = rule.reason or ( + f"Matched policy rule tool='{rule.tool}' -> effect={rule.effect}" + ) + return PolicyResult(effect=rule.effect, reason=reason, matched_rule=rule) + return PolicyResult( + effect=self.default_effect, + reason=( + f"No policy rule matched tool '{context.tool_name}'; " + f"default effect is '{self.default_effect}'" + ), + ) diff --git a/src/agentic_sidecar/gate/risk.py b/src/agentic_sidecar/gate/risk.py index fec2a1b..bcd7b18 100644 --- a/src/agentic_sidecar/gate/risk.py +++ b/src/agentic_sidecar/gate/risk.py @@ -1,9 +1,127 @@ """Risk Evaluator -- classifies a proposed action's risk level. -v0.1-v0.2: static rules only (tool name, argument pattern, destination). -Promote to an optional small local model only once there's measured -evidence the rule-based version is the bottleneck (ROADMAP.md's Design -Constraint 4 -- a risk classifier is not a free lunch). +v0.1-v0.2: static rules only (tool name, argument pattern). Promote to an +optional small local model only once there's measured evidence the +rule-based version is the bottleneck (ROADMAP.md's Design Constraint 4 -- +a risk classifier is not a free lunch). +""" + +from __future__ import annotations + +import fnmatch +from pathlib import Path +from typing import Any -Planned for v0.1 -- see ROADMAP.md. Not implemented yet. +import yaml +from pydantic import BaseModel, model_validator + +from agentic_sidecar.core.context import DecisionContext +from agentic_sidecar.core.decision import RiskLevel +from agentic_sidecar.core.operators import ArgOp, compare + +RISK_ORDER: dict[RiskLevel, int] = {"LOW": 0, "MEDIUM": 1, "HIGH": 2} +"""Ordinal ranking used by `Sidecar` to compare a classified risk level +against `risk_block_threshold` (core/sidecar.py). """ + + +class RiskRule(BaseModel): + """One risk classification rule. + + `tool` is an `fnmatch` glob matched against `DecisionContext.tool_name`. + The optional `arg_name`/`arg_op`/`arg_value` triple adds an argument + pattern check (e.g. "risk is HIGH when `amount` `gt` `500`") -- + ROADMAP.md's Design Constraint 4 names "tool name / argument pattern / + destination" as the intended static-rule surface. A rule with no + `arg_name` matches on tool name alone. Rules are evaluated in order; the + first match wins. + """ + + tool: str + risk: RiskLevel + reason: str | None = None + arg_name: str | None = None + arg_op: ArgOp = "eq" + arg_value: Any = None + + @model_validator(mode="after") + def _require_arg_name_for_argument_checks(self) -> RiskRule: + if self.arg_value is not None and self.arg_name is None: + raise ValueError("arg_value is set but arg_name is missing") + return self + + +class RiskResult(BaseModel): + """What the Risk Evaluator decided for one `DecisionContext`, and why.""" + + risk: RiskLevel + reason: str + matched_rule: RiskRule | None = None + + +class RiskEvaluator: + """Rule-based risk classifier. + + With no rules configured, every action resolves to `default_risk` + (`"LOW"` by default). Use `from_yaml` / `from_mapping` to load a real + rule set, e.g.: + + ```yaml + default: LOW + rules: + - tool: "issue_refund" + arg_name: amount + arg_op: gt + arg_value: 500 + risk: HIGH + reason: "Refund exceeds the standard authorization limit" + - tool: "delete_*" + risk: HIGH + ``` + """ + + def __init__( + self, + rules: list[RiskRule] | None = None, + *, + default_risk: RiskLevel = "LOW", + ) -> None: + self.rules = rules or [] + self.default_risk = default_risk + + @classmethod + def from_yaml(cls, path: str | Path) -> RiskEvaluator: + """Load a rule set from a YAML file (see class docstring for shape).""" + data = yaml.safe_load(Path(path).read_text()) + return cls.from_mapping(data or {}) + + @classmethod + def from_mapping(cls, data: dict[str, Any]) -> RiskEvaluator: + """Load a rule set from an already-parsed mapping (e.g. for tests + that don't want to touch the filesystem). + """ + rules = [RiskRule.model_validate(rule) for rule in data.get("rules", [])] + default_risk = data.get("default", "LOW") + return cls(rules, default_risk=default_risk) + + def evaluate(self, context: DecisionContext) -> RiskResult: + for rule in self.rules: + if not fnmatch.fnmatch(context.tool_name, rule.tool): + continue + if rule.arg_name is not None and not self._arg_matches(rule, context): + continue + reason = rule.reason or (f"Matched risk rule tool='{rule.tool}' -> risk={rule.risk}") + return RiskResult(risk=rule.risk, reason=reason, matched_rule=rule) + return RiskResult( + risk=self.default_risk, + reason=( + f"No risk rule matched tool '{context.tool_name}'; " + f"default risk is '{self.default_risk}'" + ), + ) + + @staticmethod + def _arg_matches(rule: RiskRule, context: DecisionContext) -> bool: + if rule.arg_name not in context.tool_args: + return False + return compare(context.tool_args[rule.arg_name], rule.arg_op, rule.arg_value) diff --git a/src/agentic_sidecar/intent/__init__.py b/src/agentic_sidecar/intent/__init__.py index abe06c1..7dd508b 100644 --- a/src/agentic_sidecar/intent/__init__.py +++ b/src/agentic_sidecar/intent/__init__.py @@ -1,11 +1,28 @@ -"""Intent Guardian -- structured IntentEnvelope, intent injection at decision -boundaries, drift detection, and constraint validation. +"""Intent Guardian: `IntentEnvelope`, `ConstraintBinding`, and alignment +scoring/drift detection against an active envelope (concept.md §6-9). -Answers "is this actually what the human asked you to accomplish?" -- the -project's actual differentiator (see README.md's "How the Decision Gate -evaluates a decision"). Keep this module's checks semantic, not a -permission list in disguise; static allow/deny checks belong in -`gate/policy.py` instead (ROADMAP.md's Design Constraint 5). - -Planned for v0.2 -- see ROADMAP.md. Not implemented yet. +Implemented as of v0.2. Answers "is this actually what the human asked you +to accomplish?" -- deliberately separate from `gate/policy.py`'s "are you +permitted to do this?" (see README.md's "How the Decision Gate evaluates a +decision" and ROADMAP.md's Design Constraint 5: a check that's really a +static allow/deny belongs in `gate/policy.py`, not here). """ + +from agentic_sidecar.intent.alignment import ( + AlignmentFinding, + AlignmentResult, + ConstraintBinding, + IntentGuardian, + evaluate_alignment, +) +from agentic_sidecar.intent.envelope import IntentEnvelope, Requester + +__all__ = [ + "AlignmentFinding", + "AlignmentResult", + "ConstraintBinding", + "IntentEnvelope", + "IntentGuardian", + "Requester", + "evaluate_alignment", +] diff --git a/src/agentic_sidecar/intent/alignment.py b/src/agentic_sidecar/intent/alignment.py index f38c73b..9fbb4d1 100644 --- a/src/agentic_sidecar/intent/alignment.py +++ b/src/agentic_sidecar/intent/alignment.py @@ -1,10 +1,148 @@ -"""Intent-drift detection and constraint validation -- is the current -proposed action still consistent with the active IntentEnvelope's goal, -constraints, and granted authority? +"""Intent Guardian -- drift detection and constraint validation against an +active `IntentEnvelope` (concept.md §8/§9). -Deterministic where possible (e.g. `proposed_refund > envelope.constraints.maximum_refund`) -so this stays checkable without an LLM -- see the v0.2.x Early Validation -Benchmark in ROADMAP.md, which measures exactly this module's catch rate. - -Planned for v0.2 -- see ROADMAP.md. Not implemented yet. +v0.2 scope is deliberately narrow: constraint validation only (numeric/ +enum/allow-list fields, e.g. `maximum_refund: 500` vs. a proposed `850`) -- +concept.md §9 and ROADMAP.md's v0.2 scope both frame this as the *first* +concrete semantic-authorization check, not the only one ever needed. +Authority-based blocking (`IntentEnvelope.authority`, e.g. +`refund_allowed: false`) is left for a later version: the envelope already +carries the field (see envelope.py), but there's no concrete tool-to- +authority binding shape yet, and Design Constraint 4's spirit applies here +too -- that shape deserves a real scenario before being built, not +speculative generality. """ + +from __future__ import annotations + +import fnmatch +from typing import Literal + +from pydantic import BaseModel, Field + +from agentic_sidecar.core.context import DecisionContext +from agentic_sidecar.core.operators import ArgOp, compare +from agentic_sidecar.intent.envelope import IntentEnvelope + +AlignmentStatus = Literal["ALLOW", "WARN", "BLOCK"] + + +class ConstraintBinding(BaseModel): + """Binds one `IntentEnvelope.constraints` entry to a specific tool + argument so it can be checked deterministically -- a constraint alone + (e.g. `maximum_refund: 500`) doesn't say which tool call it applies to. + + A binding produces a finding when the tool argument does *not* satisfy + `actual constraint_value`. E.g. `ConstraintBinding( + constraint="maximum_refund", tool="issue_refund", arg_name="amount", + op="lte")` flags any `issue_refund` call whose `amount` exceeds the + envelope's `maximum_refund`. A binding for a constraint the active + envelope doesn't declare, or a tool call missing the bound argument, + simply doesn't match -- same "no rule matched" philosophy as + `gate/policy.py` and `gate/risk.py`. + """ + + constraint: str + tool: str + arg_name: str + op: ArgOp = "lte" + severity: Literal["WARN", "BLOCK"] = "BLOCK" + reason: str | None = None + + +class AlignmentFinding(BaseModel): + """One violated binding, or the envelope-expiry check.""" + + status: Literal["WARN", "BLOCK"] + reason: str + constraint: str | None = None + + +class AlignmentResult(BaseModel): + """What Intent Guardian decided for one `DecisionContext`, and why. + + `status` is the single most severe finding (`BLOCK` beats `WARN` beats + `ALLOW`); `findings` keeps every finding that fired, for callers that + want the full picture rather than just the headline verdict. + """ + + status: AlignmentStatus + reason: str + findings: list[AlignmentFinding] = Field(default_factory=list) + + +def evaluate_alignment( + context: DecisionContext, + envelope: IntentEnvelope, + bindings: list[ConstraintBinding] | None = None, +) -> AlignmentResult: + """Check one proposed action against an `IntentEnvelope`: is the + envelope still valid (not expired), and does the action satisfy every + `ConstraintBinding` that applies to it? + """ + findings: list[AlignmentFinding] = [] + + if envelope.expires is not None and envelope.is_expired(): + findings.append( + AlignmentFinding( + status="WARN", + reason=f"Intent envelope expired at {envelope.expires.isoformat()}", + ) + ) + + for binding in bindings or []: + if not fnmatch.fnmatch(context.tool_name, binding.tool): + continue + if binding.constraint not in envelope.constraints: + continue + if binding.arg_name not in context.tool_args: + continue + constraint_value = envelope.constraints[binding.constraint] + actual = context.tool_args[binding.arg_name] + if compare(actual, binding.op, constraint_value): + continue # satisfies the constraint + reason = binding.reason or ( + f"'{binding.arg_name}'={actual!r} on '{context.tool_name}' violates " + f"constraint '{binding.constraint}' (must be {binding.op} {constraint_value!r})" + ) + findings.append( + AlignmentFinding(status=binding.severity, reason=reason, constraint=binding.constraint) + ) + + if not findings: + return AlignmentResult( + status="ALLOW", + reason=f"No intent constraints violated for '{context.tool_name}'", + ) + + blocking = [f for f in findings if f.status == "BLOCK"] + if blocking: + return AlignmentResult( + status="BLOCK", + reason="; ".join(f.reason for f in blocking), + findings=findings, + ) + return AlignmentResult( + status="WARN", + reason="; ".join(f.reason for f in findings), + findings=findings, + ) + + +class IntentGuardian: + """Wraps an active `IntentEnvelope` plus its `ConstraintBinding`s -- + mirrors `PolicyAdvisor`/`RiskEvaluator`'s shape (`gate/policy.py`, + `gate/risk.py`) for a consistent construction pattern across Decision + Gate modules: `Sidecar(intent=IntentGuardian(envelope, bindings), ...)`. + """ + + def __init__( + self, + envelope: IntentEnvelope, + bindings: list[ConstraintBinding] | None = None, + ) -> None: + self.envelope = envelope + self.bindings = bindings or [] + + def evaluate(self, context: DecisionContext) -> AlignmentResult: + return evaluate_alignment(context, self.envelope, self.bindings) diff --git a/src/agentic_sidecar/intent/envelope.py b/src/agentic_sidecar/intent/envelope.py index b5a5fc1..844f772 100644 --- a/src/agentic_sidecar/intent/envelope.py +++ b/src/agentic_sidecar/intent/envelope.py @@ -1,10 +1,88 @@ -"""`IntentEnvelope` -- goal, requester, constraints, granted/denied -authority, and expiry (see README.md § Sidecar modules for the YAML shape). +"""`IntentEnvelope` -- the structured, portable representation of what a +human (or a delegating agent) actually asked for: goal, requester, +constraints, granted/denied authority, and expiry (concept.md §6). -When this is actually designed, account for the v1.0 target of publishing -it as a cross-project interoperability schema (README.md's "Long-term: an -intent propagation layer") -- e.g. a `parent_intent_id` for delegation -chains -- even though nothing consumes that field until v1.0. - -Planned for v0.2 -- see ROADMAP.md. Not implemented yet. +Informal `ai-operations-spec` alignment note (ROADMAP.md's Cross-Project +Dependencies -- formal publication as a versioned schema is a v1.0 target, +not v0.2): the field shape here -- `goal` / `requested_by {type, id}` / +`constraints` / `authority` / `expires` -- mirrors what an AIOS operational- +intent object is expected to need, so the shape shouldn't require reworking +later. `metadata` is the escape hatch for task-specific context +(concept.md's worked example's `customer`/`reason` fields) that doesn't yet +have a settled home in that alignment. """ + +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Any, Literal +from uuid import uuid4 + +from pydantic import BaseModel, Field, field_validator + +from agentic_sidecar.core.context import IntentSnapshot + + +class Requester(BaseModel): + """Who asked for this -- concept.md §6's `requested_by: {type, id}`.""" + + type: Literal["human", "agent", "system"] = "human" + id: str + + +class IntentEnvelope(BaseModel): + """See concept.md §6 for the worked YAML example this mirrors. + + `constraints` and `authority` are intentionally freeform dicts here -- + what actually *enforces* them against a specific tool call is a list of + `ConstraintBinding`s (see `agentic_sidecar.intent.alignment`), not the + envelope itself. This keeps the envelope's shape stable (and + AIOS-alignable) independent of how any one Sidecar happens to wire its + bindings. v0.2 only builds enforcement for `constraints`; `authority` + is carried on the model now (matching concept.md §6's shape) but has no + binding/enforcement mechanism yet -- see alignment.py's module + docstring for why that's deliberately deferred. + """ + + id: str = Field(default_factory=lambda: str(uuid4())) + goal: str + requested_by: Requester + constraints: dict[str, Any] = Field(default_factory=dict) + authority: dict[str, bool] = Field(default_factory=dict) + expires: datetime | None = None + metadata: dict[str, Any] = Field(default_factory=dict) + + @field_validator("expires") + @classmethod + def _require_timezone_aware_expires(cls, value: datetime | None) -> datetime | None: + if value is not None and value.tzinfo is None: + raise ValueError( + "expires must be timezone-aware (e.g. " + "datetime(..., tzinfo=timezone.utc)) -- a naive datetime " + "can't be compared against datetime.now(timezone.utc) in " + "is_expired() without silently guessing which timezone was " + "intended, and that comparison would otherwise only fail " + "deep inside evaluate(), swallowed by on_sidecar_failure." + ) + return value + + def is_expired(self, *, now: datetime | None = None) -> bool: + """True once `now` (default: the current UTC time) is at or past + `expires`. Always `False` for an envelope with no `expires` set -- + an envelope isn't stale by default, only once it's told when it + stops being valid. + """ + if self.expires is None: + return False + current = now if now is not None else datetime.now(timezone.utc) + if current.tzinfo is None: + raise ValueError("`now` must be timezone-aware if provided.") + return current >= self.expires + + def to_snapshot(self) -> IntentSnapshot: + """The lightweight view attached to `DecisionContext.intent` at + evaluation time (concept.md §7, Intent Injection: only the + relevant intent needs to reach a decision boundary, not the whole + envelope) -- goal and constraints only. + """ + return IntentSnapshot(goal=self.goal, constraints=dict(self.constraints)) diff --git a/tests/test_alignment.py b/tests/test_alignment.py new file mode 100644 index 0000000..c51fb29 --- /dev/null +++ b/tests/test_alignment.py @@ -0,0 +1,137 @@ +"""Tests for `agentic_sidecar.intent.alignment` -- constraint validation and +drift detection against an `IntentEnvelope` (the refund-limit scenario from +concept.md §9, reproduced here as the primary worked example). +""" + +from datetime import datetime, timedelta, timezone + +from agentic_sidecar.core.context import DecisionContext +from agentic_sidecar.intent.alignment import ConstraintBinding, IntentGuardian, evaluate_alignment +from agentic_sidecar.intent.envelope import IntentEnvelope, Requester + +REFUND_BINDING = ConstraintBinding( + constraint="maximum_refund", + tool="issue_refund", + arg_name="amount", + op="lte", + severity="BLOCK", +) + + +def _refund_envelope(**overrides: object) -> IntentEnvelope: + defaults: dict[str, object] = { + "goal": "refund_customer", + "requested_by": Requester(type="human", id="user123"), + "constraints": {"maximum_refund": 500}, + } + defaults.update(overrides) + return IntentEnvelope.model_validate(defaults) + + +def test_within_constraint_allows() -> None: + envelope = _refund_envelope() + context = DecisionContext(tool_name="issue_refund", tool_args={"amount": 120}) + result = evaluate_alignment(context, envelope, [REFUND_BINDING]) + assert result.status == "ALLOW" + assert result.findings == [] + + +def test_exceeding_constraint_blocks() -> None: + envelope = _refund_envelope() + context = DecisionContext(tool_name="issue_refund", tool_args={"amount": 850}) + result = evaluate_alignment(context, envelope, [REFUND_BINDING]) + assert result.status == "BLOCK" + assert "maximum_refund" in result.reason + assert len(result.findings) == 1 + assert result.findings[0].constraint == "maximum_refund" + + +def test_binding_for_unrelated_tool_does_not_match() -> None: + envelope = _refund_envelope() + context = DecisionContext(tool_name="look_up_order", tool_args={"order_id": "A100"}) + result = evaluate_alignment(context, envelope, [REFUND_BINDING]) + assert result.status == "ALLOW" + + +def test_binding_for_constraint_the_envelope_does_not_declare_does_not_match() -> None: + envelope = _refund_envelope(constraints={}) + context = DecisionContext(tool_name="issue_refund", tool_args={"amount": 850}) + result = evaluate_alignment(context, envelope, [REFUND_BINDING]) + assert result.status == "ALLOW" + + +def test_binding_for_missing_arg_does_not_match() -> None: + envelope = _refund_envelope() + context = DecisionContext(tool_name="issue_refund", tool_args={}) + result = evaluate_alignment(context, envelope, [REFUND_BINDING]) + assert result.status == "ALLOW" + + +def test_warn_severity_binding_does_not_block() -> None: + binding = REFUND_BINDING.model_copy(update={"severity": "WARN"}) + envelope = _refund_envelope() + context = DecisionContext(tool_name="issue_refund", tool_args={"amount": 850}) + result = evaluate_alignment(context, envelope, [binding]) + assert result.status == "WARN" + + +def test_block_finding_wins_over_warn_finding() -> None: + warn_binding = ConstraintBinding( + constraint="soft_limit", tool="issue_refund", arg_name="amount", op="lte", severity="WARN" + ) + envelope = _refund_envelope(constraints={"maximum_refund": 500, "soft_limit": 200}) + context = DecisionContext(tool_name="issue_refund", tool_args={"amount": 850}) + result = evaluate_alignment(context, envelope, [REFUND_BINDING, warn_binding]) + assert result.status == "BLOCK" + assert len(result.findings) == 2 + + +def test_enum_allow_list_constraint_via_in_operator() -> None: + binding = ConstraintBinding( + constraint="allowed_environments", + tool="delete_resource", + arg_name="environment", + op="in", + severity="BLOCK", + ) + envelope = _refund_envelope( + goal="clean_unused_resources", constraints={"allowed_environments": ["dev"]} + ) + prod_call = DecisionContext(tool_name="delete_resource", tool_args={"environment": "prod"}) + dev_call = DecisionContext(tool_name="delete_resource", tool_args={"environment": "dev"}) + assert evaluate_alignment(prod_call, envelope, [binding]).status == "BLOCK" + assert evaluate_alignment(dev_call, envelope, [binding]).status == "ALLOW" + + +def test_expired_envelope_warns_even_with_no_bindings() -> None: + past = datetime.now(timezone.utc) - timedelta(days=1) + envelope = _refund_envelope(expires=past) + context = DecisionContext(tool_name="issue_refund", tool_args={"amount": 10}) + result = evaluate_alignment(context, envelope, []) + assert result.status == "WARN" + assert "expired" in result.reason + + +def test_expiry_warn_does_not_hide_a_constraint_block() -> None: + past = datetime.now(timezone.utc) - timedelta(days=1) + envelope = _refund_envelope(expires=past) + context = DecisionContext(tool_name="issue_refund", tool_args={"amount": 850}) + result = evaluate_alignment(context, envelope, [REFUND_BINDING]) + assert result.status == "BLOCK" + assert len(result.findings) == 2 + + +def test_no_bindings_and_not_expired_allows() -> None: + envelope = _refund_envelope() + context = DecisionContext(tool_name="issue_refund", tool_args={"amount": 999999}) + result = evaluate_alignment(context, envelope, None) + assert result.status == "ALLOW" + + +def test_intent_guardian_wraps_evaluate_alignment() -> None: + envelope = _refund_envelope() + guardian = IntentGuardian(envelope, [REFUND_BINDING]) + over_limit = DecisionContext(tool_name="issue_refund", tool_args={"amount": 850}) + within_limit = DecisionContext(tool_name="issue_refund", tool_args={"amount": 100}) + assert guardian.evaluate(over_limit).status == "BLOCK" + assert guardian.evaluate(within_limit).status == "ALLOW" diff --git a/tests/test_context.py b/tests/test_context.py new file mode 100644 index 0000000..be4179f --- /dev/null +++ b/tests/test_context.py @@ -0,0 +1,37 @@ +"""Tests for `agentic_sidecar.core.context.DecisionContext`.""" + +from agentic_sidecar.core.context import DecisionContext, HistoryEntry, IntentSnapshot + + +def test_defaults_are_empty_dicts() -> None: + context = DecisionContext(tool_name="read_order") + assert context.tool_args == {} + assert context.metadata == {} + assert context.intent is None + assert context.history == [] + + +def test_intent_and_history_round_trip() -> None: + snapshot = IntentSnapshot(goal="refund_customer", constraints={"maximum_refund": 500}) + history = [HistoryEntry(tool_name="look_up_order", tool_args={}, status="ALLOW")] + context = DecisionContext(tool_name="issue_refund", intent=snapshot, history=history) + assert context.intent is not None + assert context.intent.goal == "refund_customer" + assert context.history[0].status == "ALLOW" + + +def test_tool_args_and_metadata_are_preserved() -> None: + context = DecisionContext( + tool_name="issue_refund", + tool_args={"order_id": "A100", "amount": 850.0}, + metadata={"trace_id": "abc123"}, + ) + assert context.tool_args == {"order_id": "A100", "amount": 850.0} + assert context.metadata == {"trace_id": "abc123"} + + +def test_separate_instances_do_not_share_default_dicts() -> None: + a = DecisionContext(tool_name="a") + b = DecisionContext(tool_name="b") + a.tool_args["x"] = 1 + assert b.tool_args == {} diff --git a/tests/test_decision.py b/tests/test_decision.py new file mode 100644 index 0000000..914dfb7 --- /dev/null +++ b/tests/test_decision.py @@ -0,0 +1,45 @@ +"""Tests for `agentic_sidecar.core.decision.Decision`.""" + +import pytest +from pydantic import ValidationError + +from agentic_sidecar.core.decision import Decision + + +def test_allow_decision_round_trips() -> None: + decision = Decision(status="ALLOW", risk="LOW", reason="no rule matched") + assert decision.status == "ALLOW" + assert decision.risk == "LOW" + assert decision.reason == "no rule matched" + + +def test_block_decision_round_trips() -> None: + decision = Decision(status="BLOCK", risk="HIGH", reason="denied by policy") + assert decision.status == "BLOCK" + assert decision.risk == "HIGH" + + +def test_warn_decision_round_trips() -> None: + decision = Decision(status="WARN", risk="LOW", reason="intent envelope expired") + assert decision.status == "WARN" + + +def test_risk_may_be_none_for_failure_path() -> None: + decision = Decision(status="BLOCK", risk=None, reason="sidecar evaluation raised") + assert decision.risk is None + + +def test_decision_is_frozen() -> None: + decision = Decision(status="ALLOW", risk="LOW", reason="ok") + with pytest.raises(ValidationError): + decision.status = "BLOCK" # type: ignore[misc] + + +def test_invalid_status_rejected() -> None: + with pytest.raises(ValidationError): + Decision(status="REPLAN", risk="LOW", reason="not until v0.4") # type: ignore[arg-type] + + +def test_invalid_risk_rejected() -> None: + with pytest.raises(ValidationError): + Decision(status="ALLOW", risk="CRITICAL", reason="not a real level") # type: ignore[arg-type] diff --git a/tests/test_envelope.py b/tests/test_envelope.py new file mode 100644 index 0000000..1c2a326 --- /dev/null +++ b/tests/test_envelope.py @@ -0,0 +1,89 @@ +"""Tests for `agentic_sidecar.intent.envelope.IntentEnvelope`.""" + +from datetime import datetime, timedelta, timezone + +import pytest +from pydantic import ValidationError + +from agentic_sidecar.intent.envelope import IntentEnvelope, Requester + + +def _envelope(**overrides: object) -> IntentEnvelope: + defaults: dict[str, object] = { + "goal": "refund_customer", + "requested_by": Requester(type="human", id="user123"), + "constraints": {"maximum_refund": 500}, + } + defaults.update(overrides) + return IntentEnvelope.model_validate(defaults) + + +def test_id_is_auto_generated_and_unique() -> None: + a, b = _envelope(), _envelope() + assert a.id + assert a.id != b.id + + +def test_defaults() -> None: + envelope = _envelope() + assert envelope.authority == {} + assert envelope.metadata == {} + assert envelope.expires is None + + +def test_never_expires_with_no_expires_set() -> None: + envelope = _envelope() + assert envelope.is_expired() is False + + +def test_is_expired_in_the_past() -> None: + past = datetime.now(timezone.utc) - timedelta(days=1) + envelope = _envelope(expires=past) + assert envelope.is_expired() is True + + +def test_is_expired_in_the_future() -> None: + future = datetime.now(timezone.utc) + timedelta(days=1) + envelope = _envelope(expires=future) + assert envelope.is_expired() is False + + +def test_is_expired_accepts_explicit_now_for_determinism() -> None: + expires = datetime(2026, 8, 10, tzinfo=timezone.utc) + envelope = _envelope(expires=expires) + before = datetime(2026, 8, 9, tzinfo=timezone.utc) + at = datetime(2026, 8, 10, tzinfo=timezone.utc) + after = datetime(2026, 8, 11, tzinfo=timezone.utc) + assert envelope.is_expired(now=before) is False + assert envelope.is_expired(now=at) is True # at-or-past expiry counts as expired + assert envelope.is_expired(now=after) is True + + +def test_to_snapshot_carries_goal_and_constraints_only() -> None: + envelope = _envelope( + goal="clean_unused_resources", + constraints={"allowed_environments": ["dev"]}, + metadata={"customer": "C8291"}, + ) + snapshot = envelope.to_snapshot() + assert snapshot.goal == "clean_unused_resources" + assert snapshot.constraints == {"allowed_environments": ["dev"]} + + +def test_to_snapshot_does_not_share_the_constraints_dict() -> None: + envelope = _envelope() + snapshot = envelope.to_snapshot() + snapshot.constraints["extra"] = 1 + assert "extra" not in envelope.constraints + + +def test_naive_expires_rejected_at_construction() -> None: + naive = datetime(2026, 8, 14, 12, 0, 0) + with pytest.raises(ValidationError, match="timezone-aware"): + _envelope(expires=naive) + + +def test_is_expired_rejects_a_naive_explicit_now() -> None: + envelope = _envelope(expires=datetime.now(timezone.utc) + timedelta(days=1)) + with pytest.raises(ValueError, match="timezone-aware"): + envelope.is_expired(now=datetime(2026, 8, 14, 12, 0, 0)) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000..4ca9b12 --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,22 @@ +"""Tests for `agentic_sidecar.core.exceptions.SidecarBlockedError`.""" + +from agentic_sidecar.core.context import DecisionContext +from agentic_sidecar.core.decision import Decision +from agentic_sidecar.core.exceptions import SidecarBlockedError + + +def test_message_includes_tool_name_args_and_reason() -> None: + context = DecisionContext(tool_name="issue_refund", tool_args={"amount": 850.0}) + decision = Decision(status="BLOCK", risk="HIGH", reason="exceeds refund limit") + error = SidecarBlockedError(decision, context) + assert "issue_refund" in str(error) + assert "850.0" in str(error) + assert "exceeds refund limit" in str(error) + assert error.decision is decision + assert error.context is context + + +def test_is_a_runtime_error() -> None: + context = DecisionContext(tool_name="anything") + decision = Decision(status="BLOCK", risk=None, reason="blocked") + assert isinstance(SidecarBlockedError(decision, context), RuntimeError) diff --git a/tests/test_langgraph_adapter.py b/tests/test_langgraph_adapter.py new file mode 100644 index 0000000..790aaf7 --- /dev/null +++ b/tests/test_langgraph_adapter.py @@ -0,0 +1,192 @@ +"""Tests for `agentic_sidecar.adapters.langgraph`. + +These exercise the wrapping/interception logic directly against plain +Python callables -- no `langgraph` install required (the adapter module has +no import-time dependency on the `langgraph` package; see its docstring). +An end-to-end example against a real LangGraph graph lives in `examples/`. +""" + +import inspect +from datetime import datetime, timedelta, timezone + +import pytest + +from agentic_sidecar.adapters.langgraph import _bind_args, attach +from agentic_sidecar.core.exceptions import SidecarBlockedError +from agentic_sidecar.core.sidecar import Sidecar +from agentic_sidecar.gate.policy import PolicyAdvisor, PolicyRule +from agentic_sidecar.gate.risk import RiskEvaluator, RiskRule +from agentic_sidecar.intent.alignment import ConstraintBinding, IntentGuardian +from agentic_sidecar.intent.envelope import IntentEnvelope, Requester + + +def read_order(order_id: str) -> str: + """Look up an order by id.""" + return f"order {order_id}: shipped" + + +def issue_refund(order_id: str, amount: float) -> str: + """Issue a refund for an order.""" + return f"refunded {amount} for {order_id}" + + +def test_wrapped_tool_preserves_name_and_docstring() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed") + (wrapped,) = attach(sidecar, [read_order]) + assert wrapped.__name__ == "read_order" + assert wrapped.__doc__ == "Look up an order by id." + + +def test_wrapped_tool_still_returns_the_real_result() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed") + (wrapped,) = attach(sidecar, [read_order]) + assert wrapped(order_id="A100") == "order A100: shipped" + + +def test_wrapped_call_is_recorded_as_a_decision() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed") + (wrapped,) = attach(sidecar, [read_order]) + wrapped(order_id="A100") + assert len(sidecar.decisions) == 1 + context, decision = sidecar.decisions[0] + assert context.tool_name == "read_order" + assert context.tool_args == {"order_id": "A100"} + assert decision.status == "ALLOW" + + +def test_positional_and_keyword_args_both_bind_correctly() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed") + (wrapped,) = attach(sidecar, [issue_refund]) + wrapped("A100", 850.0) + context, _ = sidecar.decisions[0] + assert context.tool_args == {"order_id": "A100", "amount": 850.0} + + +def test_observe_mode_still_executes_a_blocked_call() -> None: + """v0.1 is Observe mode only -- a BLOCK decision is logged/recorded but + does not stop the wrapped call (ROADMAP.md's v0.1 scope).""" + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + policy=PolicyAdvisor([PolicyRule(tool="issue_refund", effect="deny")]), + ) + (wrapped,) = attach(sidecar, [issue_refund]) + + result = wrapped(order_id="A100", amount=850.0) + + assert result == "refunded 850.0 for A100" + _, decision = sidecar.decisions[0] + assert decision.status == "BLOCK" + + +def test_high_risk_argument_pattern_is_still_observed_only() -> None: + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + risk=RiskEvaluator( + [ + RiskRule( + tool="issue_refund", arg_name="amount", arg_op="gt", arg_value=500, risk="HIGH" + ) + ] + ), + ) + (wrapped,) = attach(sidecar, [issue_refund]) + + wrapped(order_id="A100", amount=850.0) + + _, decision = sidecar.decisions[0] + assert decision.status == "BLOCK" + assert decision.risk == "HIGH" + + +def test_govern_mode_blocks_and_does_not_call_the_real_tool() -> None: + executed = [] + + def issue_refund_tracked(order_id: str, amount: float) -> str: + executed.append((order_id, amount)) + return f"refunded {amount} for {order_id}" + + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + policy=PolicyAdvisor([PolicyRule(tool="issue_refund_tracked", effect="deny")]), + mode="govern", + ) + (wrapped,) = attach(sidecar, [issue_refund_tracked]) + + with pytest.raises(SidecarBlockedError) as exc_info: + wrapped(order_id="A100", amount=850.0) + + assert executed == [] + assert exc_info.value.decision.status == "BLOCK" + assert exc_info.value.context.tool_name == "issue_refund_tracked" + + +def test_govern_mode_block_exception_carries_the_evaluated_context() -> None: + """`SidecarBlockedError.context` must reflect what Intent Guardian + actually saw (injected intent/history), not the bare pre-evaluation + context the adapter originally built.""" + envelope = IntentEnvelope( + goal="refund_customer", + requested_by=Requester(id="user123"), + constraints={"maximum_refund": 500}, + ) + binding = ConstraintBinding( + constraint="maximum_refund", tool="issue_refund", arg_name="amount", op="lte" + ) + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + roles=["policy", "risk", "intent_guardian"], + intent=IntentGuardian(envelope, [binding]), + mode="govern", + ) + (wrapped,) = attach(sidecar, [issue_refund]) + + with pytest.raises(SidecarBlockedError) as exc_info: + wrapped(order_id="A100", amount=850.0) + + assert exc_info.value.context.intent is not None + assert exc_info.value.context.intent.goal == "refund_customer" + + +def test_govern_mode_still_allows_a_permitted_call() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed", mode="govern") + (wrapped,) = attach(sidecar, [read_order]) + assert wrapped(order_id="A100") == "order A100: shipped" + + +def test_govern_mode_does_not_block_on_warn() -> None: + """WARN is advisory in both modes -- only BLOCK stops the call.""" + past = datetime.now(timezone.utc) - timedelta(days=1) + envelope = IntentEnvelope( + goal="refund_customer", requested_by=Requester(id="user123"), expires=past + ) + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + roles=["policy", "risk", "intent_guardian"], + intent=IntentGuardian(envelope), + mode="govern", + ) + (wrapped,) = attach(sidecar, [issue_refund]) + + result = wrapped(order_id="A100", amount=10.0) + + assert result == "refunded 10.0 for A100" + _, decision = sidecar.decisions[0] + assert decision.status == "WARN" + + +def test_bind_args_falls_back_to_raw_kwargs_on_mismatch() -> None: + """A call that doesn't bind against the tool's declared signature + shouldn't crash Decision Context construction -- fall back to the raw + kwargs rather than raising (see `_bind_args`'s docstring comment).""" + signature = inspect.signature(read_order) + result = _bind_args(signature, (), {"unexpected": "extra"}) + assert result == {"unexpected": "extra"} + + +def test_attach_returns_a_new_list_without_mutating_input() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed") + tools = [read_order, issue_refund] + wrapped = attach(sidecar, tools) + assert wrapped is not tools + assert wrapped[0] is not read_order + assert tools == [read_order, issue_refund] diff --git a/tests/test_operators.py b/tests/test_operators.py new file mode 100644 index 0000000..3e65803 --- /dev/null +++ b/tests/test_operators.py @@ -0,0 +1,42 @@ +"""Tests for `agentic_sidecar.core.operators.compare`.""" + +import pytest + +from agentic_sidecar.core.operators import compare + + +@pytest.mark.parametrize( + ("actual", "op", "expected", "result"), + [ + (500, "eq", 500, True), + (500, "eq", 501, False), + (500, "ne", 501, True), + (500, "ne", 500, False), + (850, "gt", 500, True), + (500, "gt", 500, False), + (500, "gte", 500, True), + (100, "gte", 500, False), + (100, "lt", 500, True), + (500, "lt", 500, False), + (500, "lte", 500, True), + (850, "lte", 500, False), + ("offshore-1", "in", ["offshore-1", "offshore-2"], True), + ("onshore", "in", ["offshore-1", "offshore-2"], False), + ("prod-db-1", "contains", "prod", True), + ("dev-db-1", "contains", "prod", False), + ], +) +def test_compare_operators(actual: object, op: str, expected: object, result: bool) -> None: + assert compare(actual, op, expected) is result # type: ignore[arg-type] + + +def test_type_mismatch_returns_false_instead_of_raising() -> None: + assert compare("not-a-number", "gt", 500) is False + + +def test_unhashable_or_wrong_container_for_in_returns_false() -> None: + assert compare(500, "in", None) is False + + +def test_contains_on_non_container_returns_false() -> None: + assert compare(500, "contains", "prod") is False diff --git a/tests/test_policy.py b/tests/test_policy.py new file mode 100644 index 0000000..845fca9 --- /dev/null +++ b/tests/test_policy.py @@ -0,0 +1,80 @@ +"""Tests for `agentic_sidecar.gate.policy.PolicyAdvisor`.""" + +from pathlib import Path + +from agentic_sidecar.core.context import DecisionContext +from agentic_sidecar.gate.policy import PolicyAdvisor, PolicyRule + + +def test_empty_policy_defaults_to_allow() -> None: + advisor = PolicyAdvisor() + result = advisor.evaluate(DecisionContext(tool_name="anything")) + assert result.effect == "allow" + assert result.matched_rule is None + + +def test_default_effect_can_be_deny() -> None: + advisor = PolicyAdvisor(default_effect="deny") + result = advisor.evaluate(DecisionContext(tool_name="anything")) + assert result.effect == "deny" + + +def test_matching_deny_rule_wins() -> None: + advisor = PolicyAdvisor([PolicyRule(tool="delete_*", effect="deny", reason="destructive")]) + result = advisor.evaluate(DecisionContext(tool_name="delete_database")) + assert result.effect == "deny" + assert result.reason == "destructive" + assert result.matched_rule is not None + assert result.matched_rule.tool == "delete_*" + + +def test_non_matching_rule_falls_through_to_default() -> None: + advisor = PolicyAdvisor([PolicyRule(tool="delete_*", effect="deny")]) + result = advisor.evaluate(DecisionContext(tool_name="read_order")) + assert result.effect == "allow" + assert result.matched_rule is None + + +def test_first_matching_rule_wins() -> None: + advisor = PolicyAdvisor( + [ + PolicyRule(tool="tool_a", effect="deny"), + PolicyRule(tool="tool_a", effect="allow"), + ] + ) + result = advisor.evaluate(DecisionContext(tool_name="tool_a")) + assert result.effect == "deny" + + +def test_auto_generated_reason_when_none_given() -> None: + advisor = PolicyAdvisor([PolicyRule(tool="tool_a", effect="deny")]) + result = advisor.evaluate(DecisionContext(tool_name="tool_a")) + assert "tool_a" in result.reason + assert "deny" in result.reason + + +def test_from_mapping_builds_rules_and_default() -> None: + advisor = PolicyAdvisor.from_mapping( + { + "default": "deny", + "rules": [{"tool": "read_*", "effect": "allow"}], + } + ) + assert advisor.default_effect == "deny" + assert advisor.evaluate(DecisionContext(tool_name="read_order")).effect == "allow" + assert advisor.evaluate(DecisionContext(tool_name="write_order")).effect == "deny" + + +def test_from_yaml_reads_a_file(tmp_path: Path) -> None: + policy_file = tmp_path / "policies.yaml" + policy_file.write_text( + "default: allow\n" + "rules:\n" + " - tool: 'delete_*'\n" + " effect: deny\n" + " reason: 'no destructive ops'\n" + ) + advisor = PolicyAdvisor.from_yaml(policy_file) + result = advisor.evaluate(DecisionContext(tool_name="delete_database")) + assert result.effect == "deny" + assert result.reason == "no destructive ops" diff --git a/tests/test_risk.py b/tests/test_risk.py new file mode 100644 index 0000000..ad6d3a1 --- /dev/null +++ b/tests/test_risk.py @@ -0,0 +1,165 @@ +"""Tests for `agentic_sidecar.gate.risk.RiskEvaluator`.""" + +from pathlib import Path + +import pytest +from pydantic import ValidationError + +from agentic_sidecar.core.context import DecisionContext +from agentic_sidecar.gate.risk import RISK_ORDER, RiskEvaluator, RiskRule + + +def test_empty_risk_evaluator_defaults_to_low() -> None: + evaluator = RiskEvaluator() + result = evaluator.evaluate(DecisionContext(tool_name="anything")) + assert result.risk == "LOW" + + +def test_default_risk_is_configurable() -> None: + evaluator = RiskEvaluator(default_risk="MEDIUM") + result = evaluator.evaluate(DecisionContext(tool_name="anything")) + assert result.risk == "MEDIUM" + + +def test_tool_name_only_rule_matches() -> None: + evaluator = RiskEvaluator([RiskRule(tool="delete_*", risk="HIGH")]) + result = evaluator.evaluate(DecisionContext(tool_name="delete_database")) + assert result.risk == "HIGH" + assert result.matched_rule is not None + + +def test_argument_pattern_gt_matches_over_threshold() -> None: + evaluator = RiskEvaluator( + [ + RiskRule( + tool="issue_refund", + arg_name="amount", + arg_op="gt", + arg_value=500, + risk="HIGH", + reason="over refund limit", + ) + ] + ) + over = DecisionContext(tool_name="issue_refund", tool_args={"amount": 850}) + result = evaluator.evaluate(over) + assert result.risk == "HIGH" + assert result.reason == "over refund limit" + + +def test_argument_pattern_does_not_match_under_threshold() -> None: + evaluator = RiskEvaluator( + [ + RiskRule( + tool="issue_refund", + arg_name="amount", + arg_op="gt", + arg_value=500, + risk="HIGH", + ) + ], + default_risk="LOW", + ) + under = DecisionContext(tool_name="issue_refund", tool_args={"amount": 100}) + result = evaluator.evaluate(under) + assert result.risk == "LOW" + + +def test_missing_arg_never_matches() -> None: + evaluator = RiskEvaluator( + [RiskRule(tool="issue_refund", arg_name="amount", arg_op="gt", arg_value=500, risk="HIGH")], + default_risk="LOW", + ) + result = evaluator.evaluate(DecisionContext(tool_name="issue_refund", tool_args={})) + assert result.risk == "LOW" + + +def test_type_mismatch_never_matches_instead_of_raising() -> None: + evaluator = RiskEvaluator( + [RiskRule(tool="issue_refund", arg_name="amount", arg_op="gt", arg_value=500, risk="HIGH")], + default_risk="LOW", + ) + result = evaluator.evaluate( + DecisionContext(tool_name="issue_refund", tool_args={"amount": "not-a-number"}) + ) + assert result.risk == "LOW" + + +def test_in_operator() -> None: + evaluator = RiskEvaluator( + [ + RiskRule( + tool="transfer_funds", + arg_name="destination", + arg_op="in", + arg_value=["offshore-1", "offshore-2"], + risk="HIGH", + ) + ] + ) + result = evaluator.evaluate( + DecisionContext(tool_name="transfer_funds", tool_args={"destination": "offshore-1"}) + ) + assert result.risk == "HIGH" + + +def test_first_matching_rule_wins() -> None: + evaluator = RiskEvaluator( + [ + RiskRule(tool="tool_a", risk="HIGH"), + RiskRule(tool="tool_a", risk="LOW"), + ] + ) + result = evaluator.evaluate(DecisionContext(tool_name="tool_a")) + assert result.risk == "HIGH" + + +@pytest.mark.parametrize( + ("arg_op", "arg_value", "actual", "expected_match"), + [ + ("ne", 500, 100, True), + ("ne", 500, 500, False), + ("gte", 500, 500, True), + ("lt", 500, 100, True), + ("lte", 500, 500, True), + ("contains", "prod", "prod-db-1", True), + ("contains", "prod", "dev-db-1", False), + ], +) +def test_remaining_comparison_operators( + arg_op: str, arg_value: object, actual: object, expected_match: bool +) -> None: + evaluator = RiskEvaluator( + [ + RiskRule( + tool="tool_a", arg_name="value", arg_op=arg_op, arg_value=arg_value, risk="HIGH" + ) + ], # type: ignore[arg-type] + default_risk="LOW", + ) + result = evaluator.evaluate(DecisionContext(tool_name="tool_a", tool_args={"value": actual})) + assert result.risk == ("HIGH" if expected_match else "LOW") + + +def test_arg_value_without_arg_name_is_rejected() -> None: + with pytest.raises(ValidationError, match="arg_name"): + RiskRule(tool="tool_a", arg_value=500, risk="HIGH") + + +def test_risk_order_is_ascending() -> None: + assert RISK_ORDER["LOW"] < RISK_ORDER["MEDIUM"] < RISK_ORDER["HIGH"] + + +def test_from_yaml_reads_a_file(tmp_path: Path) -> None: + risk_file = tmp_path / "risk_rules.yaml" + risk_file.write_text( + "default: LOW\n" + "rules:\n" + " - tool: 'delete_*'\n" + " risk: HIGH\n" + " reason: 'destructive by default'\n" + ) + evaluator = RiskEvaluator.from_yaml(risk_file) + result = evaluator.evaluate(DecisionContext(tool_name="delete_database")) + assert result.risk == "HIGH" + assert result.reason == "destructive by default" diff --git a/tests/test_sidecar.py b/tests/test_sidecar.py new file mode 100644 index 0000000..3cb7719 --- /dev/null +++ b/tests/test_sidecar.py @@ -0,0 +1,360 @@ +"""Tests for `agentic_sidecar.core.sidecar.Sidecar`, including the +`on_sidecar_failure` fail_open/fail_closed paths (ROADMAP.md's Design +Constraint 3 -- these must always be exercised). +""" + +from datetime import datetime, timedelta, timezone + +import pytest + +from agentic_sidecar.core.context import DecisionContext +from agentic_sidecar.core.decision import Decision +from agentic_sidecar.core.sidecar import Sidecar +from agentic_sidecar.gate.policy import PolicyAdvisor, PolicyRule +from agentic_sidecar.gate.risk import RiskEvaluator, RiskResult, RiskRule +from agentic_sidecar.intent.alignment import AlignmentResult, ConstraintBinding, IntentGuardian +from agentic_sidecar.intent.envelope import IntentEnvelope, Requester + +REFUND_BINDING = ConstraintBinding( + constraint="maximum_refund", tool="issue_refund", arg_name="amount", op="lte" +) + + +def _refund_guardian(**envelope_overrides: object) -> IntentGuardian: + defaults: dict[str, object] = { + "goal": "refund_customer", + "requested_by": Requester(type="human", id="user123"), + "constraints": {"maximum_refund": 500}, + } + defaults.update(envelope_overrides) + envelope = IntentEnvelope.model_validate(defaults) + return IntentGuardian(envelope, [REFUND_BINDING]) + + +def test_on_sidecar_failure_is_required() -> None: + with pytest.raises(TypeError): + Sidecar() # type: ignore[call-arg] + + +def test_on_sidecar_failure_rejects_invalid_value() -> None: + with pytest.raises(ValueError, match="on_sidecar_failure"): + Sidecar(on_sidecar_failure="maybe") # type: ignore[arg-type] + + +def test_invalid_risk_block_threshold_rejected_at_construction() -> None: + with pytest.raises(ValueError, match="risk_block_threshold"): + Sidecar(on_sidecar_failure="fail_open", risk_block_threshold="SEVERE") # type: ignore[arg-type] + + +def test_invalid_mode_rejected() -> None: + with pytest.raises(ValueError, match="mode"): + Sidecar(on_sidecar_failure="fail_closed", mode="advise") # type: ignore[arg-type] + + +def test_govern_mode_is_accepted() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed", mode="govern") + assert sidecar.mode == "govern" + + +def test_evaluate_never_raises_for_block_regardless_of_mode() -> None: + """`evaluate()` only computes a Decision -- enforcing a BLOCK (actually + stopping the call) is the attached adapter's job, not core's. See + `agentic_sidecar.core.exceptions.SidecarBlockedError`.""" + for mode in ("observe", "govern"): + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + policy=PolicyAdvisor(default_effect="deny"), + mode=mode, # type: ignore[arg-type] + ) + decision = sidecar.evaluate(DecisionContext(tool_name="anything")) + assert decision.status == "BLOCK" + + +def test_default_deny_all_policy_blocks() -> None: + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + policy=PolicyAdvisor(default_effect="deny"), + ) + decision = sidecar.evaluate(DecisionContext(tool_name="anything")) + assert decision.status == "BLOCK" + assert "Policy Advisor" in decision.reason + + +def test_allowed_low_risk_action_allows() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed") + decision = sidecar.evaluate(DecisionContext(tool_name="read_order")) + assert decision.status == "ALLOW" + assert decision.risk == "LOW" + + +def test_high_risk_action_blocks_at_default_threshold() -> None: + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + risk=RiskEvaluator([RiskRule(tool="delete_*", risk="HIGH")]), + ) + decision = sidecar.evaluate(DecisionContext(tool_name="delete_database")) + assert decision.status == "BLOCK" + assert decision.risk == "HIGH" + + +def test_medium_risk_allows_when_threshold_is_high() -> None: + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + risk=RiskEvaluator([RiskRule(tool="archive_*", risk="MEDIUM")]), + risk_block_threshold="HIGH", + ) + decision = sidecar.evaluate(DecisionContext(tool_name="archive_order")) + assert decision.status == "ALLOW" + assert decision.risk == "MEDIUM" + + +def test_custom_risk_block_threshold_blocks_medium() -> None: + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + risk=RiskEvaluator([RiskRule(tool="archive_*", risk="MEDIUM")]), + risk_block_threshold="MEDIUM", + ) + decision = sidecar.evaluate(DecisionContext(tool_name="archive_order")) + assert decision.status == "BLOCK" + + +def test_policy_deny_short_circuits_before_risk_runs() -> None: + calls: list[str] = [] + + class _TrackingRiskEvaluator(RiskEvaluator): + def evaluate(self, context: DecisionContext) -> RiskResult: + calls.append(context.tool_name) + return super().evaluate(context) + + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + policy=PolicyAdvisor([PolicyRule(tool="delete_*", effect="deny")]), + risk=_TrackingRiskEvaluator(), + ) + decision = sidecar.evaluate(DecisionContext(tool_name="delete_database")) + assert decision.status == "BLOCK" + assert calls == [] + + +def test_fail_open_allows_when_hook_raises() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_open") + + @sidecar.before_tool_call + def broken_hook(context: DecisionContext) -> Decision: + raise RuntimeError("evaluator exploded") + + decision = sidecar.evaluate(DecisionContext(tool_name="anything")) + assert decision.status == "ALLOW" + assert decision.risk is None + assert "RuntimeError" in decision.reason + + +def test_fail_closed_blocks_when_hook_raises() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed") + + @sidecar.before_tool_call + def broken_hook(context: DecisionContext) -> Decision: + raise RuntimeError("evaluator exploded") + + decision = sidecar.evaluate(DecisionContext(tool_name="anything")) + assert decision.status == "BLOCK" + assert decision.risk is None + + +def test_custom_hook_overrides_default_evaluation() -> None: + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + policy=PolicyAdvisor(default_effect="deny"), # would otherwise block everything + ) + + @sidecar.before_tool_call + def always_allow(context: DecisionContext) -> Decision: + return Decision(status="ALLOW", risk="LOW", reason="custom hook allows everything") + + decision = sidecar.evaluate(DecisionContext(tool_name="anything")) + assert decision.status == "ALLOW" + assert decision.reason == "custom hook allows everything" + + +def test_every_evaluation_is_recorded() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed") + context = DecisionContext(tool_name="read_order") + decision = sidecar.evaluate(context) + assert sidecar.decisions == [(context, decision)] + + +def test_unknown_role_rejected() -> None: + with pytest.raises(ValueError, match="Unknown role"): + Sidecar(on_sidecar_failure="fail_closed", roles=["not_a_role"]) + + +def test_future_role_raises_not_implemented() -> None: + with pytest.raises(NotImplementedError, match="v0.3"): + Sidecar(on_sidecar_failure="fail_closed", roles=["planner"]) + + +def test_disabling_policy_role_skips_it() -> None: + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + policy=PolicyAdvisor(default_effect="deny"), + roles=["risk"], + ) + decision = sidecar.evaluate(DecisionContext(tool_name="anything")) + assert decision.status == "ALLOW" + + +def test_disabling_risk_role_skips_it() -> None: + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + risk=RiskEvaluator(default_risk="HIGH"), + roles=["policy"], + ) + decision = sidecar.evaluate(DecisionContext(tool_name="anything")) + assert decision.status == "ALLOW" + assert decision.risk is None + + +# --- Intent Guardian (v0.2) -------------------------------------------------- + + +def test_intent_without_role_enabled_raises() -> None: + with pytest.raises(ValueError, match="intent_guardian"): + Sidecar( + on_sidecar_failure="fail_closed", + intent=_refund_guardian(), + roles=["policy", "risk"], + ) + + +def test_set_intent_without_role_enabled_raises() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed", roles=["policy", "risk"]) + with pytest.raises(ValueError, match="intent_guardian"): + sidecar.set_intent(_refund_guardian()) + + +def test_intent_guardian_role_enabled_but_no_envelope_yet_is_a_noop() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed", roles=["policy", "risk", "intent_guardian"]) + decision = sidecar.evaluate( + DecisionContext(tool_name="issue_refund", tool_args={"amount": 999}) + ) + assert decision.status == "ALLOW" + + +def test_intent_guardian_blocks_refund_over_limit() -> None: + """The refund-limit scenario from concept.md §9, end to end through + `Sidecar.evaluate()`.""" + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + roles=["policy", "risk", "intent_guardian"], + intent=_refund_guardian(), + ) + decision = sidecar.evaluate( + DecisionContext(tool_name="issue_refund", tool_args={"amount": 850}) + ) + assert decision.status == "BLOCK" + assert "Intent Guardian" in decision.reason + assert "maximum_refund" in decision.reason + + +def test_intent_guardian_allows_refund_within_limit() -> None: + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + roles=["policy", "risk", "intent_guardian"], + intent=_refund_guardian(), + ) + decision = sidecar.evaluate( + DecisionContext(tool_name="issue_refund", tool_args={"amount": 120}) + ) + assert decision.status == "ALLOW" + + +def test_intent_guardian_warns_on_expired_envelope() -> None: + past = datetime.now(timezone.utc) - timedelta(days=1) + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + roles=["policy", "risk", "intent_guardian"], + intent=_refund_guardian(expires=past), + ) + decision = sidecar.evaluate(DecisionContext(tool_name="issue_refund", tool_args={"amount": 10})) + assert decision.status == "WARN" + assert "expired" in decision.reason + + +def test_set_intent_updates_the_active_envelope() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed", roles=["policy", "risk", "intent_guardian"]) + over_limit = DecisionContext(tool_name="issue_refund", tool_args={"amount": 850}) + assert sidecar.evaluate(over_limit).status == "ALLOW" # no envelope set yet + + sidecar.set_intent(_refund_guardian()) + assert sidecar.evaluate(over_limit).status == "BLOCK" + + sidecar.set_intent(None) + assert sidecar.evaluate(over_limit).status == "ALLOW" + + +def test_policy_and_risk_still_short_circuit_before_intent_guardian_runs() -> None: + calls: list[str] = [] + + class _TrackingGuardian(IntentGuardian): + def evaluate(self, context: DecisionContext) -> AlignmentResult: + calls.append(context.tool_name) + return super().evaluate(context) + + envelope = _refund_guardian().envelope + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + policy=PolicyAdvisor([PolicyRule(tool="issue_refund", effect="deny")]), + roles=["policy", "risk", "intent_guardian"], + intent=_TrackingGuardian(envelope, [REFUND_BINDING]), + ) + decision = sidecar.evaluate( + DecisionContext(tool_name="issue_refund", tool_args={"amount": 850}) + ) + assert decision.status == "BLOCK" + assert calls == [] + + +def test_evaluate_injects_intent_snapshot_into_recorded_context() -> None: + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + roles=["policy", "risk", "intent_guardian"], + intent=_refund_guardian(), + ) + sidecar.evaluate(DecisionContext(tool_name="issue_refund", tool_args={"amount": 100})) + recorded_context, _ = sidecar.decisions[0] + assert recorded_context.intent is not None + assert recorded_context.intent.goal == "refund_customer" + assert recorded_context.intent.constraints == {"maximum_refund": 500} + + +def test_evaluate_mutates_the_callers_context_in_place() -> None: + """`evaluate()` must inject intent/history onto the exact object the + caller passed in (not a copy) -- an adapter building + `SidecarBlockedError` after the call relies on this to report the + context Intent Guardian actually saw.""" + sidecar = Sidecar( + on_sidecar_failure="fail_closed", + roles=["policy", "risk", "intent_guardian"], + intent=_refund_guardian(), + ) + context = DecisionContext(tool_name="issue_refund", tool_args={"amount": 100}) + assert context.intent is None + + sidecar.evaluate(context) + + assert context.intent is not None + assert context.intent.goal == "refund_customer" + assert context is sidecar.decisions[0][0] + + +def test_evaluate_injects_growing_history_into_recorded_context() -> None: + sidecar = Sidecar(on_sidecar_failure="fail_closed") + sidecar.evaluate(DecisionContext(tool_name="look_up_order")) + sidecar.evaluate(DecisionContext(tool_name="issue_refund", tool_args={"amount": 10})) + + first_context, _ = sidecar.decisions[0] + second_context, _ = sidecar.decisions[1] + assert first_context.history == [] + assert len(second_context.history) == 1 + assert second_context.history[0].tool_name == "look_up_order" + assert second_context.history[0].status == "ALLOW" diff --git a/uv.lock b/uv.lock index e48cc58..0be4311 100644 --- a/uv.lock +++ b/uv.lock @@ -23,10 +23,11 @@ wheels = [ [[package]] name = "agentic-sidecar" -version = "0.0.1" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "pydantic" }, + { name = "pyyaml" }, ] [package.optional-dependencies] @@ -43,6 +44,10 @@ dev = [ { name = "pytest-cov" }, { name = "ruff" }, { name = "twine" }, + { name = "types-pyyaml" }, +] +langgraph = [ + { name = "langgraph" }, ] [package.metadata] @@ -50,14 +55,17 @@ requires-dist = [ { name = "agentic-chaos", marker = "extra == 'agentic-chaos'", specifier = ">=0.1" }, { name = "agenticlens", marker = "extra == 'agenticlens'", specifier = ">=0.1.3" }, { name = "build", marker = "extra == 'dev'", specifier = ">=1.2" }, + { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=1.0,<2" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" }, { name = "pydantic", specifier = ">=2.0,<3" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" }, + { name = "pyyaml", specifier = ">=6.0,<7" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, { name = "twine", marker = "extra == 'dev'", specifier = ">=5.1" }, + { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0" }, ] -provides-extras = ["agenticlens", "agentic-chaos", "dev"] +provides-extras = ["agenticlens", "agentic-chaos", "langgraph", "dev"] [[package]] name = "agenticlens" @@ -94,6 +102,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/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.8.0" @@ -528,6 +550,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, ] +[[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" @@ -549,6 +580,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[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 = "id" version = "1.6.1" @@ -636,6 +704,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" @@ -682,6 +771,123 @@ 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.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { 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/e5/c1/c226367fdf92a173c8b520d0b3583a0aaf4c3fe4952010f680e49d88589d/langchain_core-1.5.5.tar.gz", hash = "sha256:c08d78176113867e9a76acc1007d641cd68fa5682e9e0018980d062b4ae0777e", size = 983166, upload-time = "2026-08-14T18:56:24.364Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/09/9f29e62e99118865d092acda6143a1ba6ce1893e3dcb72388a75b798e37d/langchain_core-1.5.5-py3-none-any.whl", hash = "sha256:537037fd44ead7c1ffb9621011029ad2e347e051519d17f112d7bf7be12f4365", size = 565884, upload-time = "2026-08-14T18:56:23.051Z" }, +] + +[[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.11" +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/56/0d/c8e7ee98896659e1b6555db0ab115a9ca899844744645d5d894032bab1d7/langgraph-1.2.11.tar.gz", hash = "sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363", size = 725753, upload-time = "2026-08-11T14:00:36.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7f/c5c30e4be99ff821029c7ac872a480676bb179c9f3df85ea3f38d13f86d4/langgraph-1.2.11-py3-none-any.whl", hash = "sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b", size = 248854, upload-time = "2026-08-11T14:00:35.494Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/e1/089c4c9e0a2fec7f883f82ae8e6a727138d50074cfeb6644bc2d13b1019b/langgraph_checkpoint-4.2.0.tar.gz", hash = "sha256:51a593b6bee684b0818e5d6e58e28ab340c6db7794575056ce7bd1b746a84ed7", size = 180239, upload-time = "2026-08-07T20:05:03.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/71/3b475f09bd57d3a5649792c66353312b4432afd843f301739dfcebd157f0/langgraph_checkpoint-4.2.0-py3-none-any.whl", hash = "sha256:0547fd228935a0b758865de3a3d6d7a2537c308895d0f9ab092ce9151b5da942", size = 56833, upload-time = "2026-08-07T20:05:02.655Z" }, +] + +[[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.11.0" +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/5b/62/a917e87073767db8ca335c3f5a42fb4c5336509b8d5b03e165e46da47e70/langsmith-0.11.0.tar.gz", hash = "sha256:7339f90e6fd9a1a009445b5084a7a0e56a8b6f17305ee5d7e8c5e7582217854f", size = 4805612, upload-time = "2026-08-14T12:56:55.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/aa/18d334f04c12bb9154568747539a1143a93d0c34b894662c065454f396c8/langsmith-0.11.0-py3-none-any.whl", hash = "sha256:e87a3929915936c066b3fa3283ec3f3f0013e2ef7f98a443a7fbe3fab8e784a3", size = 737950, upload-time = "2026-08-14T12:56:53.014Z" }, +] + [[package]] name = "librt" version = "0.15.0" @@ -945,6 +1151,134 @@ 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.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/742fb1f62b825f2c010697eaf4e828004bc2a81e7e806666989c132c7c42/orjson-3.12.0.tar.gz", hash = "sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5", size = 4142915, upload-time = "2026-08-14T16:13:30.607Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/35/819eeb4fa8ee676d38fdbb8213a76fd496f7dbbfdfafa89d34e02b22dfac/orjson-3.12.0-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:747843254519dd43b93eee3153a19e5a509334320c4d2f823ec879232db5c796", size = 224133, upload-time = "2026-08-14T16:12:00.607Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/d9221d4a2b085b073fcddc91728d490f20b9cf010c62c2f42371ab997695/orjson-3.12.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:7c2ad193c8004254f34b499f3bd2c80f043d10754aff2b38f93da574f4883f98", size = 113669, upload-time = "2026-08-14T16:12:02.126Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/644cbbcabb26df61d9ef0c66e6f2bf8b687cc7b66137597f2858951f1952/orjson-3.12.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:bc7a872f03522d90e0429e6c0c5cd23084f767bedcb4c58048eec19294613344", size = 130410, upload-time = "2026-08-14T16:12:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/14/6d/e3a8c34d687895aecd8b267a01c46106eb98d8424a83bfa7bacb723854f6/orjson-3.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18a87929f31d94a77f7dc93cf527e91f39ce7fe7813d588a4de2507efd32a387", size = 131101, upload-time = "2026-08-14T16:12:04.918Z" }, + { url = "https://files.pythonhosted.org/packages/75/20/930824c07685c22af23f26818ed3853b0270488a412b6ab757904b7f787b/orjson-3.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9683ee9ea0659da64f36574ef675b8a86330c34c19ea75db1fb93c3ff99e0ef", size = 131479, upload-time = "2026-08-14T16:12:06.11Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a6/22e863bbbe8917aa292e33e0db597000f9a07eb5e6f52efed623fa16bae1/orjson-3.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:103b5db66aa53c1f9e88c2524be4f383e831ba7dfd5f9f5af6336a177c622f11", size = 135865, upload-time = "2026-08-14T16:12:07.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/a0/ceb5008914a65e9a19a46a09d94bc67a74d120209fdfa772750023ceb377/orjson-3.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd57d79aefa3f84eec851d6de7a366795b9345cfaf17f82b4820430a7a5fa241", size = 127843, upload-time = "2026-08-14T16:12:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/3d/61c6b3b84c250cb09cb7229701ff77e4d763773ad7f577d0b6abf2892664/orjson-3.12.0-cp310-cp310-win32.whl", hash = "sha256:3dbce9b6b3074b31a5d5dd322a9c4e5b16f206091ece4194c2e36952847a105e", size = 128293, upload-time = "2026-08-14T16:12:09.819Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/ea0f4a563253b6363195a4f704123c6bfbf156641bd3be5a75de81c5e917/orjson-3.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:3bb17a06f9bd15237b3216c044209fe92597379124018cfc196fbb846cde64df", size = 122216, upload-time = "2026-08-14T16:12:11.261Z" }, + { url = "https://files.pythonhosted.org/packages/75/1a/a7075a8e8b0d3f5097d17ac3099017104b6b7b42012041147995d5b2da05/orjson-3.12.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a94f0f0c6fcbb2b5bd9734c57a489c7584a732bbdf04a39e8c83b861e9d03e92", size = 223409, upload-time = "2026-08-14T16:12:12.654Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c2eb3b2900e5597db7841a4c6416ac2d90081bd956b02d4dd1833fa2b96b/orjson-3.12.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:a696529ec96a90d9a5f9570207efe403c8b08f8e4aa2783ee3403511e2fdfa10", size = 124015, upload-time = "2026-08-14T16:12:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/1c/df/b49081766a75b6a37b3d33bdc0a39e492abab8441dd25e3e1998e7b83fcb/orjson-3.12.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:e4ac5059baab4b3acbd99485de019ff8cda0fdf34b61fa74f7197a53db78bfe8", size = 113471, upload-time = "2026-08-14T16:12:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/48/d4/58ea28eeef95c2a27358ed927380a621162cf20bd740bbccf9c3f09a200a/orjson-3.12.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e29957429c35bbb5a185a119c523aa2428b7bbf1a293724c7b9375ed8f892a3", size = 129998, upload-time = "2026-08-14T16:12:17.503Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f4/1e82aa2efc9916422d804697876ce433c907a1abd7c7e5c6d3d48565e5f9/orjson-3.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dce0166feb0a737ab84f598c9a338cbc0b764a036617aa686194f53c7eba0c3e", size = 130891, upload-time = "2026-08-14T16:12:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/15169e9d22b59a406264f99d6db387c0b0b12b6357a8a0169917c2a713eb/orjson-3.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9caf3d09f47c3c70c4451ada20ef9bc4a4cdffa26f49862cf0a253b329aae2d5", size = 131285, upload-time = "2026-08-14T16:12:20.251Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/763dbd426290d044ec3e615a05e70adb6d8b6f95bf17dc355c0081a5e8b6/orjson-3.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9dca132b1fda5565088e65a6b6e742285e0aeceb6fae549fa8863e16c7d3998", size = 135707, upload-time = "2026-08-14T16:12:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/3b2038ed168d22e14182ed715d6963f9c073a83a2ba43cfe918a4fc43c64/orjson-3.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a791f793b287bbc135b8e87c34e35c8bfc693e2a8a620fab1ae682b925f9a32e", size = 127669, upload-time = "2026-08-14T16:12:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/88/ae/b84b3d3e65f5629ada0edcb1d2bccc55d7c5f89d8b981537ecdc3d6f31ec/orjson-3.12.0-cp311-cp311-win32.whl", hash = "sha256:31ed278a36304390adc3eec5d7f6fd593a7c3e99e5a06cd07866396c4b1b4710", size = 128043, upload-time = "2026-08-14T16:12:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/35/24/2ed0e6f51ea3d0af45d807233a851175af75bec83ef5fd0d6a2601904ec0/orjson-3.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb2539159dfe8d371914f354360fa50e4a577cc89222a3828b9650a5e5040252", size = 122084, upload-time = "2026-08-14T16:12:25.813Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/95d25fcfbc9471799ef6bb01c552d64ee5cde93ee40ba2f423dd3442c708/orjson-3.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:61318b6de893c7a9d9f3e5ecbadccbfc26a7eb417ccc7bbf0771de3b4d72f868", size = 127035, upload-time = "2026-08-14T16:12:27.201Z" }, + { url = "https://files.pythonhosted.org/packages/be/4a/295da39c651c2faac8bd351a2a346f0fdedd9d50b847ee9dfc27d2207ef6/orjson-3.12.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0", size = 223427, upload-time = "2026-08-14T16:12:28.525Z" }, + { url = "https://files.pythonhosted.org/packages/29/98/758cf90fbeaaafb7f8141bfac75a432099959f3a2f5db93a412e876415d8/orjson-3.12.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54", size = 123725, upload-time = "2026-08-14T16:12:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/32/b5/5b934d251f8651f7e41df180ad0c57a6e1cabe15c7bd331638413a50ebc9/orjson-3.12.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83", size = 113375, upload-time = "2026-08-14T16:12:31.209Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/37efb5b12a176ce3ced29f4144f20da57d02757f78ce549637dc1b4e1fc8/orjson-3.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7", size = 129983, upload-time = "2026-08-14T16:12:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/0644b87c73f13e0092df8f35a1fe280d991e5e90072087411e0dd7e44e0c/orjson-3.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e", size = 130629, upload-time = "2026-08-14T16:12:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/8c/57/80b986ebfecd9c6a177ddf1c2319717f0cd8feffb2b78946595a18a2fc88/orjson-3.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b", size = 131245, upload-time = "2026-08-14T16:12:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/80/3d/75c5ac5a69161f44492a68fbdde66f4cc4ce48cd5e1fb05918e46f0c8848/orjson-3.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f", size = 135397, upload-time = "2026-08-14T16:12:37.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/93/4d71f2df314a97ff0d27a4559bf5888fc8406e3c6dec90e92291e3511215/orjson-3.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873", size = 127693, upload-time = "2026-08-14T16:12:38.627Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/0dbc6be5adfd1730491072fb60beb6bcdf5d7b2596ee41b7fc2e298bfc09/orjson-3.12.0-cp312-cp312-win32.whl", hash = "sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5", size = 128000, upload-time = "2026-08-14T16:12:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c9/97b1ce0112ebf5e949c775ed5b1755e562233179f3584579673cc24d6378/orjson-3.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a", size = 122106, upload-time = "2026-08-14T16:12:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6a/facd8b312e4a0d3a7fa978c7e15821f74a336adf1d65529faec33b48e18b/orjson-3.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d", size = 126869, upload-time = "2026-08-14T16:12:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/54/cb/d7b78218a987eb8a8ce4eeae0286b1bb679333eb631ea0eeaf6371680bfc/orjson-3.12.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900", size = 223397, upload-time = "2026-08-14T16:12:44.003Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/bc87c45e7ec639d35ebefd62618e01939531ac8e171426606a01bda05914/orjson-3.12.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03", size = 123662, upload-time = "2026-08-14T16:12:45.433Z" }, + { url = "https://files.pythonhosted.org/packages/94/ee/c9a4ff3f2dbedbbe9e635d0fa72c8866adede09b6335ef9644f53752f0d8/orjson-3.12.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8", size = 113374, upload-time = "2026-08-14T16:12:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/75/09/3f330a026a796c8b4c97a6f429652a5e912e7065039bf96ed25e42aa7b25/orjson-3.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94", size = 130029, upload-time = "2026-08-14T16:12:48.06Z" }, + { url = "https://files.pythonhosted.org/packages/7d/40/094cc53126a3d22f76cdf83b6ea67338bed01d774037621a785aa8e6e5ea/orjson-3.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806", size = 130528, upload-time = "2026-08-14T16:12:49.362Z" }, + { url = "https://files.pythonhosted.org/packages/bc/74/89bb236deb9565f99434b13052bb40ddfcce4adf3afbfa3132ee7e421468/orjson-3.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df", size = 131075, upload-time = "2026-08-14T16:12:50.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ac/1176360d762c01b5bd34acd56fc098e936c491363d8b6b397ad4aa475547/orjson-3.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978", size = 135321, upload-time = "2026-08-14T16:12:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/7a/02/bbd881c8b9276d50b998de38b4e97de8ace1aac940b0ee545aedbf65ed00/orjson-3.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222", size = 127472, upload-time = "2026-08-14T16:12:53.517Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/a0934d7503e6dcbedd6afac3e7f3f8597fd09389949ad94d0f7540e9dbca/orjson-3.12.0-cp313-cp313-win32.whl", hash = "sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1", size = 128000, upload-time = "2026-08-14T16:12:55.14Z" }, + { url = "https://files.pythonhosted.org/packages/52/87/69f98f8d40faff103a965a5fbb83f08241b01beaf92badb5413fbc9358cc/orjson-3.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2", size = 121841, upload-time = "2026-08-14T16:12:56.507Z" }, + { url = "https://files.pythonhosted.org/packages/e6/07/b83046a4e3cadcc0987d0f160696107c4af706a619b56e4ad01940cadadf/orjson-3.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e", size = 126765, upload-time = "2026-08-14T16:12:57.806Z" }, + { url = "https://files.pythonhosted.org/packages/12/9d/3931253e6f3148abf2cbe14830367042a4806b362ea520df2303db188fb9/orjson-3.12.0-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d", size = 223391, upload-time = "2026-08-14T16:12:59.184Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/b4a4f1e305367245877b967a0bad70fcf001d77c54ac4339a120b66fdae4/orjson-3.12.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647", size = 123659, upload-time = "2026-08-14T16:13:00.548Z" }, + { url = "https://files.pythonhosted.org/packages/96/f3/6782c6fa85e2702bc66be183c3b421486167dcf266ee4dc1403fe3824870/orjson-3.12.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c", size = 113337, upload-time = "2026-08-14T16:13:02.009Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/b32ab64bacda9d0fa4942ef483bd03cabf0eaf2be819ca9fb7ff610c559d/orjson-3.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc", size = 130112, upload-time = "2026-08-14T16:13:03.404Z" }, + { url = "https://files.pythonhosted.org/packages/ee/49/6e6142999ca01509219be5e5a9c338a3e5ea011f63e91ff473fbbf3734ed/orjson-3.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1", size = 130520, upload-time = "2026-08-14T16:13:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/49/d0/3745af0a4cc9867784f29722929cec4d10bd1c877cd754b01ba6d96eb21a/orjson-3.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a", size = 131053, upload-time = "2026-08-14T16:13:06.14Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/6fe5a22fa478fffb190e65c338c84df5c311ef597b363150a17cc57063c0/orjson-3.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e", size = 135321, upload-time = "2026-08-14T16:13:07.544Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/b1b0ec30289646a81a76e2dbaae2686b96fcccb7cb0323dc1dd78cbc7875/orjson-3.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f", size = 127485, upload-time = "2026-08-14T16:13:08.88Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2b/277404bdcc21c93b112b963655b76443ebfe828f8a3ff1de7d90f8850eb3/orjson-3.12.0-cp314-cp314-win32.whl", hash = "sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92", size = 128048, upload-time = "2026-08-14T16:13:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/395b36fa2b4ce7af70b651d715e88f80d884b2c2b14a6b53e84d554fb5f0/orjson-3.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed", size = 121858, upload-time = "2026-08-14T16:13:11.634Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a3/833e895ff452859eebe75093d26691fe9108f1a7a6a08435d7a5780ea652/orjson-3.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7", size = 126749, upload-time = "2026-08-14T16:13:13.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/99c8947ece10c17176af9aae85c4948f1d109da77440ec14d87239efaf73/orjson-3.12.0-cp315-cp315-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e", size = 223398, upload-time = "2026-08-14T16:13:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/cf983fe09f2731420fda097a9f7ef4343f47fa216c228961ad8f6da44f3d/orjson-3.12.0-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517", size = 123655, upload-time = "2026-08-14T16:13:16.221Z" }, + { url = "https://files.pythonhosted.org/packages/11/50/9cb8ae73fa4749dbbc20f617004213b5ff01c20aaeec34c3f31124f2c1d8/orjson-3.12.0-cp315-cp315-manylinux_2_39_aarch64.whl", hash = "sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38", size = 130515, upload-time = "2026-08-14T16:13:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0a/adb6ce1a5b5fbf9cb1790f9961bb668a0dd5429aadaf6cee044724681795/orjson-3.12.0-cp315-cp315-manylinux_2_39_armv7l.whl", hash = "sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d", size = 113327, upload-time = "2026-08-14T16:13:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/51/5c/d17f61581d8dbdde7048f87a330fa24915edec38db4d72b381fec14fbb56/orjson-3.12.0-cp315-cp315-manylinux_2_39_i686.whl", hash = "sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13", size = 130105, upload-time = "2026-08-14T16:13:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b7/938befcf33bee4704a92ecec6a2731224c539d939bf9429fd39396d28931/orjson-3.12.0-cp315-cp315-manylinux_2_39_x86_64.whl", hash = "sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328", size = 131049, upload-time = "2026-08-14T16:13:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/b0/15/cfa2021d64d5aa8bb5c9f604ef375e00ec8b657651b5dd650b1b7ad13df1/orjson-3.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c", size = 135320, upload-time = "2026-08-14T16:13:23.415Z" }, + { url = "https://files.pythonhosted.org/packages/1a/50/3e75dfe357c1e8f9e287c7a5740260ef15bd23a5299eae8d0835dcad5375/orjson-3.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a", size = 127488, upload-time = "2026-08-14T16:13:24.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/a6/79aed402eb3ab284dc5b4791a7ad62c5875127de01b8e3f04bd92d551298/orjson-3.12.0-cp315-cp315-win32.whl", hash = "sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55", size = 128048, upload-time = "2026-08-14T16:13:26.217Z" }, + { url = "https://files.pythonhosted.org/packages/64/f7/2723e264aab7248c1ed6ecaad8e5d0cb866c0cffde75442102ffa7491aba/orjson-3.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578", size = 121860, upload-time = "2026-08-14T16:13:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/630c9113ec8996778f1f0304b364b091b9a9db5fef5fdc17cca622f5ea24/orjson-3.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc", size = 126754, upload-time = "2026-08-14T16:13:28.962Z" }, +] + +[[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/93/fa/a91f70829ebccf6387c4946e0a1a109f6ba0d6a28d65f628bedfad94b890/ormsgpack-1.12.2-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c1429217f8f4d7fcb053523bbbac6bed5e981af0b85ba616e6df7cce53c19657", size = 378262, upload-time = "2026-01-18T20:55:22.284Z" }, + { url = "https://files.pythonhosted.org/packages/5f/62/3698a9a0c487252b5c6a91926e5654e79e665708ea61f67a8bdeceb022bf/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f13034dc6c84a6280c6c33db7ac420253852ea233fc3ee27c8875f8dd651163", size = 203034, upload-time = "2026-01-18T20:55:53.324Z" }, + { url = "https://files.pythonhosted.org/packages/66/3a/f716f64edc4aec2744e817660b317e2f9bb8de372338a95a96198efa1ac1/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59f5da97000c12bc2d50e988bdc8576b21f6ab4e608489879d35b2c07a8ab51a", size = 210538, upload-time = "2026-01-18T20:55:20.097Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/a436be9ce27d693d4e19fa94900028067133779f09fc45776db3f689c822/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e4459c3f27066beadb2b81ea48a076a417aafffff7df1d3c11c519190ed44f2", size = 212401, upload-time = "2026-01-18T20:55:46.447Z" }, + { url = "https://files.pythonhosted.org/packages/10/c5/cde98300fd33fee84ca71de4751b19aeeca675f0cf3c0ec4b043f40f3b76/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a1c460655d7288407ffa09065e322a7231997c0d62ce914bf3a96ad2dc6dedd", size = 387080, upload-time = "2026-01-18T20:56:00.884Z" }, + { url = "https://files.pythonhosted.org/packages/6a/31/30bf445ef827546747c10889dd254b3d84f92b591300efe4979d792f4c41/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:458e4568be13d311ef7d8877275e7ccbe06c0e01b39baaac874caaa0f46d826c", size = 482346, upload-time = "2026-01-18T20:55:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f5/e1745ddf4fa246c921b5ca253636c4c700ff768d78032f79171289159f6e/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cde5eaa6c6cbc8622db71e4a23de56828e3d876aeb6460ffbcb5b8aff91093b", size = 425178, upload-time = "2026-01-18T20:55:27.106Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a2/e6532ed7716aed03dede8df2d0d0d4150710c2122647d94b474147ccd891/ormsgpack-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc7a33be14c347893edbb1ceda89afbf14c467d593a5ee92c11de4f1666b4d4f", size = 117183, upload-time = "2026-01-18T20:55:55.52Z" }, + { 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.3" @@ -1612,6 +1946,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] +[[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 = "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" @@ -1701,6 +2053,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" }, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260815" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/72/b56089aeee6c496d969bac42376bedb6e3eeab4682e1018fa3137122f94b/types_pyyaml-6.0.12.20260815.tar.gz", hash = "sha256:28764110c9cf35846e733da32d8d734df7473c5dde9ef67c3b7332ec0e819858", size = 18545, upload-time = "2026-08-15T02:41:51.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/52/eefeba09be4ef2a1eb989eb92934561e8e502a6ee3c32654996e4be7e399/types_pyyaml-6.0.12.20260815-py3-none-any.whl", hash = "sha256:6f332212b7e191f3afd5016a713c510b6340593b7ebec573c7d5d20aa5386d3b", size = 21148, upload-time = "2026-08-15T02:41:50.555Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" @@ -1731,6 +2092,362 @@ 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/7c/60/659104207938f2ac62508b9aa595fc0515ac7452dd515c8e1d47d0b91169/uuid_utils-0.17.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d2d9a63a9e6f2416ace8c109043a9280d6b34f34bb2e5421903e149403db40a6", size = 564038, upload-time = "2026-07-09T13:47:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e7/e0d048a268b4163058bdd2f07a45bbe13c29e3cc6b7b88f8f00b001617ce/uuid_utils-0.17.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b776c7fc8755c7de06dd5a22b47c40ae84f67d13277ebb233cc84933ba4dcbcd", size = 286680, upload-time = "2026-07-09T13:47:53.141Z" }, + { url = "https://files.pythonhosted.org/packages/84/83/e3606dc9b4224d0c9a6675d9347e7e0da7e67fa30e061bfdb686138844d0/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1edf2f8732e4ed95bd7b65f2658f4aa072efaaff321144f4e0d4bf6a22709263", size = 323533, upload-time = "2026-07-09T13:47:54.433Z" }, + { url = "https://files.pythonhosted.org/packages/22/f8/aec5c34fa80c9fef09a506a098015e728080076494b72b9e8e5cfc9669c4/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84ed3a2d5cd3ae6db87af20bfed3331116195ba4757ad7177fc8f12c1bbce2a9", size = 330691, upload-time = "2026-07-09T13:47:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/08/73/85776566863514f37b0a761648368e96b07d64981a9b6c391220aa2563a9/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bf4d9cd1e80e73922073b9b27c143bedeb109d65f94cd12712e2c87118f2b7d", size = 444094, upload-time = "2026-07-09T13:47:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/06/e0424b4268c0932e0ff8257303d70de4053f05958843268fac4cb0f79b57/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52db0e471d3d2632d35445af352591f40a8f32959a412981d9f51e068bb9514b", size = 324548, upload-time = "2026-07-09T13:47:58.217Z" }, + { url = "https://files.pythonhosted.org/packages/db/d2/a0cb3a69ef6d9becc30a6a0594ddf6f798f6204953dfa85073cbec875b94/uuid_utils-0.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:344f7c755e280ea0ba6aeb08022190d867a80000b1715cacded54fc4b5633607", size = 350307, upload-time = "2026-07-09T13:47:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/82/81/d82766af7db541e4a78b920bc1c4303d44995f841805d1498934088cd12c/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:589d9da7de8fa7f739bb970ac4632c9a268213117d634e1c4a58c1c1e821ca05", size = 500661, upload-time = "2026-07-09T13:48:00.726Z" }, + { url = "https://files.pythonhosted.org/packages/10/71/b261cd0d38497ed8c2cce0263c5607ec9cd2bbace0f73cb19a6fc2060b6e/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cee808b405e9095506f4e4e89924bec7ea77eac3129b6fe36eda04364b3b343b", size = 606577, upload-time = "2026-07-09T13:48:02.539Z" }, + { url = "https://files.pythonhosted.org/packages/3b/63/9e48512bb235e9533adbb25c30fd0c9cef09f6ecefe131ba392b98572b40/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:53ce348ef4c6e98c02c19c522af01334fe94476ce9af0db8c4482f9f142ae9c1", size = 567054, upload-time = "2026-07-09T13:48:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cc/d7bad8799a37ec33fc21b29fcb459d63d9f88aa09056d0c3e58903ba2fb0/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9e753e81457241e2200c56a898e268e8fa25796271af0489c608f24d8e631eed", size = 529682, upload-time = "2026-07-09T13:48:05.097Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3b/59b1e07ada8aadd3c046c97fe9814d85e770abb7e8cf68d5d86538bf62e9/uuid_utils-0.17.0-cp310-cp310-win32.whl", hash = "sha256:c589f5023d471ce75dd2cce61acb25ed6347e562041588a1a366808f22d7176c", size = 170595, upload-time = "2026-07-09T13:48:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5c/23a2d0253ada2ee8c497d541d4ef0dd5576c3d2454ec2f9d0b8a06af9304/uuid_utils-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:981cc10163988defea96e8d6c507df151eab8f483e7df9ae543d5a41a4be073b", size = 177225, upload-time = "2026-07-09T13:48:07.561Z" }, + { 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/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { 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/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { 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 = "xxhash" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/6f/4d42b20b0dc688552f568e356e7d55b8ea18092eb79c78e40a96f59e7e12/xxhash-4.0.0.tar.gz", hash = "sha256:6b4040b26c73c75e6c12273b0ae237f9235f39cce5fabb683a64e9f20d09df8b", size = 100993, upload-time = "2026-08-12T16:59:59.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/a7/35ba744acf16381b269d2b367dbdc59ef48fa2521e52f05bd436b7466711/xxhash-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:753cebce949f8e8c54e5fcfaee8d0c3b16c7015b63fd2c7e583143d9a268c78a", size = 38083, upload-time = "2026-08-12T16:52:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e3/e19896b868bf3f0047ed66de387a102f9dcfdd5429d2a61c4f67cd9eed20/xxhash-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b403b36648e4593689ba4401796c7834d6a908dd9cd3567c8d85e486346c9446", size = 35927, upload-time = "2026-08-12T16:52:50.003Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1d/fbb54a21c59aa281c674a84ec8ecc3dbbf8381f3eb884c087217b3f28db4/xxhash-4.0.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f3a9c5bebf0c40ec933992b6a578530b44e2cdcdd1fbb9c4004be752b496db49", size = 242845, upload-time = "2026-08-12T16:52:45.117Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c4/065890de07233ec559481cc1cb28dc2dc0645748bc8d5a7fe98668987ba3/xxhash-4.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f80c50793ee34b72292ee695a935182bcb097846316957bd26daccdf1164d423", size = 265776, upload-time = "2026-08-12T16:52:46.402Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ad/02e596eccdf4facbdb380256ca522ab081e2b49aaa718d71a4ef66a6709e/xxhash-4.0.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ea6a85f49c2959121440372967042b9808f8ffa43c90f47b38dc2ba3aee7a95", size = 287000, upload-time = "2026-08-12T16:53:00.22Z" }, + { url = "https://files.pythonhosted.org/packages/cd/a9/37f1bad31a1b0a2d22795c9fffbcfa7560234ce390e3ff58d4309c446411/xxhash-4.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ab3090bdf3a2cfc58041a491012e8eb63f9e41ac02e8dff6f0094853f93dfa2", size = 270333, upload-time = "2026-08-12T16:52:44.854Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/2d7358865c1fd06903aceeb9612e03413ed32682ba6a96efa2b3216bea4f/xxhash-4.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0000c53562673f5b7b2e9ac744efde362781b5914975c0828327f42a2c1269a8", size = 497460, upload-time = "2026-08-12T16:52:45.039Z" }, + { url = "https://files.pythonhosted.org/packages/17/2a/a6fd74537c72d3fac51f4581b3113a4e881670bf101ecacd32d549a0f308/xxhash-4.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e6e3ef0a8df99291019000b594d7cd21d1b638e4fc4234c9b7f43eb7838c8c3", size = 249178, upload-time = "2026-08-12T16:52:45.021Z" }, + { url = "https://files.pythonhosted.org/packages/98/e9/6a0d9cdd601e0b99f866b4356d427ca2a130070ec3464cf6ed711baee9bd/xxhash-4.0.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:013feb67c7b83cf6fd55d013d4217edb68d7d3b86ef9d53f80d9aac7f440e4a9", size = 333009, upload-time = "2026-08-12T16:53:00.039Z" }, + { url = "https://files.pythonhosted.org/packages/13/d0/1a7207a5c8ad2bf5a328bc547de80782e340c4264da78095133f11ef4ada/xxhash-4.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:86d8c11b5fcbd97e2d6a8705ad8772e3e2e4c6eae592ee167928d2feaa930040", size = 262158, upload-time = "2026-08-12T16:52:49.231Z" }, + { url = "https://files.pythonhosted.org/packages/da/7a/11d1a989982d265ce3b4b288f2a1269c0508f2ab705f83a16e5f2df940fc/xxhash-4.0.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ec072557f5f4c5eeb6ae724820867e20759b979e7e11a714508dd70cd3641166", size = 289014, upload-time = "2026-08-12T16:52:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2e/2360a635a249aff94514b8a8cacf021085d3da9820032f806c75414716e3/xxhash-4.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1cffda52b73a9369ef9d521416f5b995a21b09f222701f6c9ce700ba84931d9c", size = 247629, upload-time = "2026-08-12T16:53:08.198Z" }, + { url = "https://files.pythonhosted.org/packages/72/9d/aa944d7fe4eb3ee6bf697c32e092744deccd5f4721f928fb0f703c9edaf5/xxhash-4.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5036cdbc161cf5a2751f7caa54f94c1d9f405436a08c4221dec12491a17a3235", size = 267531, upload-time = "2026-08-12T16:52:46.108Z" }, + { url = "https://files.pythonhosted.org/packages/d3/85/484493315182da4c26a645e749f84d42474c0082cefb08d97c89debf7706/xxhash-4.0.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:422feffe2bdaebc2f173dcc94ea3a6ba7710163c3c00b9bc9bd47d77a9c61d83", size = 322191, upload-time = "2026-08-12T16:52:46.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f8/cc5c757db7ecfed7415614d5f44f15a0a2d2bb0e1fdfb18e23519d724c33/xxhash-4.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6a9dd076149afe335505b880d8c9ebf578cc770d296379d9f58688f70c73a620", size = 466022, upload-time = "2026-08-12T16:52:47.781Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a0/403613a07590bfe733a18f3c352e52982b6b7811b6e771df2b5bfbf80c51/xxhash-4.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:980bedbeb1ed99f1febca84900aa201c7978fd43894bfa2feff9def46f3e8b10", size = 245777, upload-time = "2026-08-12T16:53:11.528Z" }, + { url = "https://files.pythonhosted.org/packages/29/3c/cbc896acc48429b4941d9b4ba2db62b2b49ddeacacd89c0baccc0155f52d/xxhash-4.0.0-cp310-cp310-win32.whl", hash = "sha256:083551d29935553b203454995a1e07e9bd6d2ebaccb6eaa25f69c6fd112ce779", size = 34288, upload-time = "2026-08-12T16:52:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/be9a63dde10cc1d9c053a3c02da81da99828cdf2a73a1cb7cdd9f3cf561e/xxhash-4.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:5ac7166fcc0b76989bc313658339ea54a6cc9c96f01b04b3f96373f63709fa39", size = 36718, upload-time = "2026-08-12T16:53:06.151Z" }, + { url = "https://files.pythonhosted.org/packages/2a/85/e915df396b2f17ce2dd9708e053773970dcdc4a31f29ec95a6a7908c867d/xxhash-4.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:c32bfed9e5f139df55fa5e8c1e568712ac13628d6373cd1cfd9657dbd0fc1fd3", size = 32956, upload-time = "2026-08-12T16:53:36.761Z" }, + { url = "https://files.pythonhosted.org/packages/40/9f/3736468bf0680ede8d93a488eba7dac58ca2ff25d6bff168810f7528c300/xxhash-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7ce6de9166b8a8c1c29d5eab91b508f4c31ae3ff8626a3ed65bd2cbbe64a40", size = 38087, upload-time = "2026-08-12T16:53:10.683Z" }, + { url = "https://files.pythonhosted.org/packages/23/27/a1370dc53ae4c10efe0eef02dfc57a613b4e104073a335fa88c3983e8b12/xxhash-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b783b3b8a7c6cb79be6be6c32cc0f9c2dc3bbf4beb013bece39b4f8fcb2b4de", size = 35925, upload-time = "2026-08-12T16:52:51.017Z" }, + { url = "https://files.pythonhosted.org/packages/d8/87/f0703078a7cae7caf210d77ad6a2d91509434c1c1496233bb304e1202933/xxhash-4.0.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:601b3c3b09b44ed2ada35674bcb19c435ba9203862a4832731384269391bfed9", size = 252935, upload-time = "2026-08-12T16:52:50.195Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/f472a6bc86208de130b2317af1aefe08d5139bd06696e7e920cf47d8b6bb/xxhash-4.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7e38e909ed1d5a86511bf2fc57891d0fea5ef64063cf7995bf954cef2365b2b", size = 276246, upload-time = "2026-08-12T16:53:14.475Z" }, + { url = "https://files.pythonhosted.org/packages/74/7c/4ff4ad2f16169070461460bdb99568eebd167932d8145f793d1cdba8077c/xxhash-4.0.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:492f267d1dedeef7c44310e60a08f38f34db75ed373062fafe68aa09801e4281", size = 295909, upload-time = "2026-08-12T16:53:08.962Z" }, + { url = "https://files.pythonhosted.org/packages/17/1d/417266187a86a340f4b8ec55d16844cbc53c20fd0503e7fb5b7a07aabd55/xxhash-4.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f2bc28050a61684f1c825ca72be56eefe1421112d1ccceb4eb99d86b52aa4c56", size = 279566, upload-time = "2026-08-12T16:53:22.923Z" }, + { url = "https://files.pythonhosted.org/packages/78/2a/c5e4987c4fa758bcaaa6fe320ecde667380e93ae206f36daa37c4545cc8f/xxhash-4.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97730a254c3d11e43dffa815a9837a622d7f2a30a9d897bd29184d5589b7dc25", size = 509522, upload-time = "2026-08-12T16:54:01.123Z" }, + { url = "https://files.pythonhosted.org/packages/61/d7/1a3e8044c94053e8230ae746218741dcad2e67ceb4ce3d55bc85f923f406/xxhash-4.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:baa9828055ebaa00b95523aabef7504c3d1685bfab48b7c2ed15d0ba56269708", size = 260625, upload-time = "2026-08-12T16:53:19.979Z" }, + { url = "https://files.pythonhosted.org/packages/24/07/05186cfaa8410f5f6d14a691db021d930a84bc2205e45d6bed61078b8af5/xxhash-4.0.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23ec6388d554e523bc06bf40dbf2dd09b74af246a6f2a6a17223a16fc575f676", size = 339347, upload-time = "2026-08-12T16:52:57.801Z" }, + { url = "https://files.pythonhosted.org/packages/3e/17/7628c83969d68287fb174aa3b993c5147c578ae60856f72ffad488cd1684/xxhash-4.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1d56a5c9fb9c6c6a6eaf0adbe398c57ab6e89348a83cb99d230a52668cea557", size = 272193, upload-time = "2026-08-12T16:52:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/a7/18/ded75ea5a74b8538d75e825558dbc0304726509890980edf0dd9884f3a02/xxhash-4.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:681892a96f5727e4a082af13fb75c1cbf1ef66f2d56cadeddf31db6bbaa09945", size = 300004, upload-time = "2026-08-12T16:53:18.853Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c5/5215550ec7690a9dbc406a6e29cb77a130bdb1942fb639d6fbf2005f11e6/xxhash-4.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4395c0a03a1336915c677fc71e1693afb1317751ea80c130b23d6292d14f75fe", size = 258893, upload-time = "2026-08-12T16:53:41.076Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a0/8d7a0addb64e32762af3f5e7838e1808a6997a516a2493648d651ba795c1/xxhash-4.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2987f0908d5e3908bc1949a0ca4f465583ba55befad5899581edfb6b59441f56", size = 277785, upload-time = "2026-08-12T16:59:00.01Z" }, + { url = "https://files.pythonhosted.org/packages/13/1c/62dd9957ed770b2ef5f8fc0527442975afd4055921ba009fef54d54f0b79/xxhash-4.0.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3de54ba80c88096fbcca29e3b255989b3bca02e47a976bf9303de2f36261c57f", size = 329548, upload-time = "2026-08-12T16:54:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/329ba182d0fdc65fce8057fa9bc0d8979e7bd31cd4cfbc4fd991d1db2db0/xxhash-4.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0a9d3fc725df9b0029094dbbe700a5d4633881f2afb87392ce5860388c22bebd", size = 477008, upload-time = "2026-08-12T16:53:37.32Z" }, + { url = "https://files.pythonhosted.org/packages/81/c9/f9041891290b403d07de8cdc66da95289e6b407bc83d16be62a6acaad5f1/xxhash-4.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:84e0b626285289e40be24eefd51ab9fef9bc6373f4025c3271b3b0e3a2ac78f3", size = 257166, upload-time = "2026-08-12T16:53:07.244Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ae/6f0f310ea658e83df21f171fed313f6be01fe918a2eec76c4ea549b79233/xxhash-4.0.0-cp311-cp311-win32.whl", hash = "sha256:6ec7a1b252ba32719605ecc2f9bd4230fdb3b5f0bb424c671961d21dd0acf7db", size = 34287, upload-time = "2026-08-12T16:53:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/02/cc/3702bf613a42923627bbad08b434237cef9b525b00341207496548cb10af/xxhash-4.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:50cf753d4d93797c2d32a72c305ab7017af4bae9bb6c999222fb29d9ea9961f7", size = 36653, upload-time = "2026-08-12T16:53:29.162Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5e/d0ccb316354dbf7d5868896e5d87bceb6f50710d6b386159294a4da4679e/xxhash-4.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:460146a030d3dfcd6cd08a0bb45c2fe10f04026f80ac16ae42a846e8d850f0ce", size = 32958, upload-time = "2026-08-12T16:53:46.82Z" }, + { url = "https://files.pythonhosted.org/packages/19/d9/493e761ee66047de5d15b7b5137972bf26d889eb58e49b1f18e2f35cea6e/xxhash-4.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d58ab471ab4442ac29ce225edf496f40d136dba552b0a718c796d14b71550b0", size = 38115, upload-time = "2026-08-12T16:59:02.316Z" }, + { url = "https://files.pythonhosted.org/packages/87/7b/a03aca88de3fae90f619ef6acd72b90e4b06e840a66ed4574558c29a080b/xxhash-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3a42fa24a47961e38ace7e4675680eb3cbd024588eb0dbc9dfbd009f4dce6bd", size = 35932, upload-time = "2026-08-12T16:54:12.151Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b4/8e935dc765a3018ada3da2455eed2e878c6cb686a9c01b3db1619a7c79b5/xxhash-4.0.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4c6eca223bcd94a9a5d291512114364facc5c4738c0d468ced386e88fa9071b", size = 252775, upload-time = "2026-08-12T16:53:43.21Z" }, + { url = "https://files.pythonhosted.org/packages/f4/aa/7c1cbcf64d8c5e9ea23808356af91a158e891e08d8f206884ceaf0425d61/xxhash-4.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7264a1a8bc4cda273347b6fead2a15c1fc070736160fe927867279a9d991604a", size = 276218, upload-time = "2026-08-12T16:53:12.936Z" }, + { url = "https://files.pythonhosted.org/packages/07/a3/5ffcecc8fb1480f147a5396ae52b27a9b519092d0ba31a4c614a5a0ecc57/xxhash-4.0.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7f9a849a801557a3a2dc9226dfd11b6911c766cb3f16d36822488204788de65b", size = 297412, upload-time = "2026-08-12T16:53:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/5b/90/c4067783a921aa055372bf2f66fccec0bb4fd06edd6ab3b752b973492d05/xxhash-4.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f9a33820c448e9fa3a720396d90ae3eff7dfa942d86af855965a095f954d0ac7", size = 279882, upload-time = "2026-08-12T16:53:38.703Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0c/a777dbcd2f10517a0f49cd276b9c6a2d1f30cc0bde0f67da65b56849a646/xxhash-4.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:76989952a47452f66ac213779b62961b8e489c98f45c4ea7d7dea9115a00d06f", size = 510640, upload-time = "2026-08-12T16:54:01.844Z" }, + { url = "https://files.pythonhosted.org/packages/22/bb/4148016c68211d8cc348eabe06e40686c71c4f7b336261f4b4e72d2d46ab/xxhash-4.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c72ee49f74745e554226018e56dd93d4ff924dfa870d119a59828d29080853", size = 261458, upload-time = "2026-08-12T16:59:04.292Z" }, + { url = "https://files.pythonhosted.org/packages/72/dc/170ee0593a11c472e32b745b6a300f2eaf531347d1053bbc411f24d3071e/xxhash-4.0.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9335b632cdec5498068372b75ab18a6c1905eb51ba76c727db46080b66ef7d4f", size = 339621, upload-time = "2026-08-12T16:54:31.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/b31e1be5d3f892eb8fa306d1bfbe7c1c5c4d6011e045351434278c2526d3/xxhash-4.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b939f78096460a1e3905cd92815d01fde6fb2c54a544daf8ea03a1fded41d74b", size = 272552, upload-time = "2026-08-12T16:53:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1f/82830fa11afb369ae8ab0692d316e68be6128d12756e22d23d0f7a5b34aa/xxhash-4.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7d6fe512a9c84e5031d1b97c7b931c7694ba7f10f44bf4466c219f468f0ed749", size = 301046, upload-time = "2026-08-12T16:53:30.172Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/b081f9ae955d452be3b8f3c17ef8d5d4484606ea871c45f73046d9eaf438/xxhash-4.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5a9656d42c8e87013655a8a668a870222235ae26cbaae69c922b12c66b672f38", size = 259794, upload-time = "2026-08-12T16:53:52.134Z" }, + { url = "https://files.pythonhosted.org/packages/1e/01/b35974b9890d5d1d83c325220f9e98482c54109a4e98e3596b9464de4813/xxhash-4.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:14b91efc9fc7072ee22b4ce0c778d41e53dd2d45ca5b16fdfe8cf33ebad3c386", size = 277906, upload-time = "2026-08-12T16:53:50.359Z" }, + { url = "https://files.pythonhosted.org/packages/84/95/78d8a7228a9caa65e7adcfe0a72c160b2e739930f90ae0b4267386d8510e/xxhash-4.0.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e07cba198b78fef4ea81b219058ee3e9ed8d612b055e0388b205a3f20e1b70e", size = 329997, upload-time = "2026-08-12T16:54:06.363Z" }, + { url = "https://files.pythonhosted.org/packages/3c/84/29f5bbbcc14a56ccfa4fe0e025ce2fa55ee4b40485a268c828c47ce52405/xxhash-4.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:963e5b1f0152193703997eabcdb7320c0d2e3f934a00be03a25dac4fb4af66dd", size = 478204, upload-time = "2026-08-12T16:59:07.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/13/56da2f5d5f2eb28b64aca54240e555221a8e2292644b60a73f46dfaaacb5/xxhash-4.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bc49915f326b888773979381140eb1a5e804fbc872670f6a2d36d8d421225a2a", size = 257845, upload-time = "2026-08-12T16:54:42.948Z" }, + { url = "https://files.pythonhosted.org/packages/f7/80/65141cc3d3e6b0cd52b4c7d9af32a6fbc3ea223995cfabf406b2f589e0c1/xxhash-4.0.0-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:89bc01208487b7f4b0b636b4e8b809ec3c41644fc38487394d9faf9570d4795b", size = 20416, upload-time = "2026-08-12T16:54:03.689Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cb/8e261dc126968573b999908fdc091705e850337af24a3492f44b3d48de58/xxhash-4.0.0-cp312-cp312-win32.whl", hash = "sha256:453c9bff5e2c6a2b938704e41965c65d1339a2e07817e1cacaf239c330af4d5e", size = 34309, upload-time = "2026-08-12T16:54:04.695Z" }, + { url = "https://files.pythonhosted.org/packages/ff/bb/edec8f095a24a331293fbdacbed9addc4f5a6f2e2f88c3ed539d7cbdb438/xxhash-4.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f8515f2e70a98382adf2c1b1e413f3bfa46a3cb1b35752928265577f958f1da", size = 36739, upload-time = "2026-08-12T16:54:02.85Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/42de8febea42413de52b2d1ac5bf0081aba71c37705a3565c0c021f46bc0/xxhash-4.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:63f3cac239fcdcdcbc56cbaf5b5164f056e8a2b1e06d61436c134628743eabfa", size = 32970, upload-time = "2026-08-12T16:54:28.673Z" }, + { url = "https://files.pythonhosted.org/packages/af/34/1a6f7f86d94084ded050f6f7b3dadb6103d8cce0bc2022f7f45f003d4b97/xxhash-4.0.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:3ad3124acd9ae0c8d069cd1879e453ded1d5b72024b1f7a90cf8fb1d30f9681a", size = 43233, upload-time = "2026-08-12T16:54:14.472Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fa/e71472c0af1c6cd710f50fdda7cd0fe2931977c40845dcf3d45ef93fd633/xxhash-4.0.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:abeae2d9e797f7bdd627df6785d4839517d1f67a0f91deb4da27ab3417e5c56c", size = 40311, upload-time = "2026-08-12T16:59:10.158Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fe/3456e5d85a89d6421d009ca97fb77a9f5cefc5c8d465232be51aa39ac591/xxhash-4.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cdf3a9494a0418d37cb1d7b490620b331b09167e5923fcde9d0bc759a87e33dc", size = 34376, upload-time = "2026-08-12T16:54:45.456Z" }, + { url = "https://files.pythonhosted.org/packages/a8/06/9cc19701019915c83767f78be0a16aa3ce0be01372f964c6545941bcf298/xxhash-4.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:af6ba29275dde4eba5fc223a7635edb88fbaf658289e1f89007e05c74204bc06", size = 35335, upload-time = "2026-08-12T16:54:07.032Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f2/871dfea505b2ed142c536046fd06b42478b50ac45bb793d3ad151dfdc9cf/xxhash-4.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:51288138c05f55fd1cb9c98c33e01eb573744e768ac342e3e938d03b0e0b9683", size = 37637, upload-time = "2026-08-12T16:54:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/57/d2/0585560cd638f267c8a6e219b1acb428e8a7e1d9170d47df2e8b8af4ecbe/xxhash-4.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0ed59c8f1eabb250cb293590dd59af835e4b6c5a2c749923ae1292c3743ec82a", size = 37735, upload-time = "2026-08-12T16:54:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a3/c65f35ea36754998e4359cd4be71b3685c8847e1ff3c7217ef5d624e237c/xxhash-4.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6838e142ac44cb0838c395db285d040784404dd33f6b7d92b2338f2d849f8b97", size = 35578, upload-time = "2026-08-12T16:54:41.782Z" }, + { url = "https://files.pythonhosted.org/packages/0f/33/6a0d39f99177f34a85f215293fbc064a7de3c2f65eb29c3300119fd7291e/xxhash-4.0.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:070fde1355b8ba1da1ab78fe78297abb919a61aea5adbf2b8ee2a2a592ce007b", size = 259108, upload-time = "2026-08-12T16:54:17.662Z" }, + { url = "https://files.pythonhosted.org/packages/24/d8/b2391845e679b0bcfa915643d89492dc80b814d526a35de03d4685fd6f70/xxhash-4.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ed50ea632e80951cedd89714505a8f85d199f2014faba4bbbc81baccc1c2081", size = 283622, upload-time = "2026-08-12T16:59:15.896Z" }, + { url = "https://files.pythonhosted.org/packages/63/ba/f0d8766eec1928a03ad9ff9c6a1447c73628f0b998968f92ea947ed6af92/xxhash-4.0.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:73274fe4597094bb5c19f9812b044d97d6ded8fefb04599bd2710dc5963fbb2f", size = 303437, upload-time = "2026-08-12T16:54:54.341Z" }, + { url = "https://files.pythonhosted.org/packages/76/c7/ad5c80fa54796328f2517899b4cfa3a94784abcd6057fb9cbedb829910fb/xxhash-4.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f0170f95d0c15c7d60cbac9b0d10dfefc01f0ecd61ce3609936495bf988189fb", size = 286862, upload-time = "2026-08-12T16:54:10.78Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/d0ff14bfda7b5e660aad7e6e342dbc3af6cc1e9e645ce26a46fe6f2f1efd/xxhash-4.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:45b905eb4116ae7ad280ba9f5b702fe77569403d53b90762b3d0509ddec3b9cd", size = 519477, upload-time = "2026-08-12T16:54:13.461Z" }, + { url = "https://files.pythonhosted.org/packages/4e/33/2e50c9a828240d76a956d2b4426f8f1e5264a60a06b84c9150b718ec8633/xxhash-4.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae29aa4ca0d8be4c13154bea4a82f77ed62062d400ead38b599b2d7bbf5b727", size = 268202, upload-time = "2026-08-12T16:54:34.635Z" }, + { url = "https://files.pythonhosted.org/packages/48/ca/f8350df6ff74963e6f7dd88edf200b17ee33dc6eba1a892f70e7f1378e2d/xxhash-4.0.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b72e2a2f65dbda433f09e1de5a116366b3305d5f4a968f44e4ac1023dd053744", size = 344705, upload-time = "2026-08-12T16:59:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/f8/70/87e0c9d65bc47a285e97024de83e237fa2309cb556a28972738dbbc9fc2d/xxhash-4.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:57ebd48125d8a469bdb167a53199213a068088a28a4e5c05bf57c15cc6eb8c7b", size = 279524, upload-time = "2026-08-12T16:54:24.045Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/1f37db24c05e316767f4cb5eb00c4ad6c5d06718a71faf5d34aac0afad01/xxhash-4.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ccbfe39040e2b4643e7e2cdef623fd45d187e430f0e6631d41101d2454cfb3e4", size = 307154, upload-time = "2026-08-12T16:59:18.85Z" }, + { url = "https://files.pythonhosted.org/packages/eb/34/771af917dbb390619083afa56c975b6e25b3256c3f8f8b93e688ceb6a42a/xxhash-4.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:edb635eb0904a04b27585a02e9030692dd0a642cdcf3f46834f598281fad246d", size = 265495, upload-time = "2026-08-12T16:55:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/b3/72/0e3d8e0a6153d22abcca15cc13e1a9a488b855e255839713347e69deca83/xxhash-4.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e9e68e32f9d7d6dacf3b7a9b7f777cbe2e2db975606e6e2af098c73ad89f09c3", size = 284019, upload-time = "2026-08-12T16:54:16.243Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/e4f1384a85479805f2dab9501b461ad6011e58aee9f74ec6a1919c228fba/xxhash-4.0.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1f9c48c63f707de4c9e76da46418838a8ab07ebd3608188389008fba264e433d", size = 335645, upload-time = "2026-08-12T16:54:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/3a/27/a0b66e349a0f8a855ff62b16ef0244c231a50f7d001fb91f0fb1616b851c/xxhash-4.0.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:07958037004350048b62ac5d088c08f83f4291c36ebe7e302f6497773903dcee", size = 486666, upload-time = "2026-08-12T16:54:43.505Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/cc99d61a81d0b3d38fdbb7c7c702a3a0577f48527b768885c0a26af06722/xxhash-4.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e54e35ff1095737599338b264d092e60c770b9c319badc47d3c61f4e037ec9b9", size = 263982, upload-time = "2026-08-12T16:59:06.51Z" }, + { url = "https://files.pythonhosted.org/packages/8d/23/f30b4d31469b1ee79316b7970e1e2b87185af16924d62ff2a77f085c0246/xxhash-4.0.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:340d30e1030d0cfd239a96b45ce3368dac061517a4c56283e3d0ac4b5da43a10", size = 20220, upload-time = "2026-08-12T16:54:41.041Z" }, + { url = "https://files.pythonhosted.org/packages/39/0f/6ff9aa9f7ec323a690fa94737be54bdcf3b28ad445d9419394a5bfc60aa3/xxhash-4.0.0-cp313-cp313-win32.whl", hash = "sha256:54c09a575b8b7c9c77ece653c6267dbef7829144358403ec2aa06cfc16d2a737", size = 34096, upload-time = "2026-08-12T16:59:20.735Z" }, + { url = "https://files.pythonhosted.org/packages/2c/39/34472a4ee5bba5abba4f5cbe6b4e6e9636a408b9e1d1dfbb856e057b193a/xxhash-4.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:c5f7268147219bad4ffa56a06a0416d30b9a4c16dd1f2fc847bcee0a82babdb7", size = 36139, upload-time = "2026-08-12T16:55:27.256Z" }, + { url = "https://files.pythonhosted.org/packages/00/8e/2b85ae94a5b112512552e6bac3c81dca62bd09344bed7eed39918152bb3a/xxhash-4.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:efde4e9568509643cf149e4d9ff8dd33aaf0e933d4e75f50549072807bd2d2c6", size = 33087, upload-time = "2026-08-12T16:54:18.504Z" }, + { url = "https://files.pythonhosted.org/packages/ab/41/588e92b0ebee8068499619795948a010d8f95eccb5a09f9f2f9917b8aae8/xxhash-4.0.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:db84ea3b737a6739a85e83369d1580735429ce77aeaa170098cae68c53808823", size = 43322, upload-time = "2026-08-12T16:54:34.351Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9f/a7842537ae06bb81e4ef56c86c761f1679d2db23bbbdae58de26bc794ff0/xxhash-4.0.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:1cd2a1f525f6de2f12f2f082f3cb3cef42c1e312391a5c775aef29e250fbe025", size = 40391, upload-time = "2026-08-12T16:54:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/a1/30/ac4bc97e28ba17c4bdc6ca7205a165aaa85d78d5b31370038217515e7364/xxhash-4.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:76e518792bab77567bbec2ffa79a832b4d6d6f3e381eebe5438f4edaedfe60e8", size = 34405, upload-time = "2026-08-12T16:59:12.225Z" }, + { url = "https://files.pythonhosted.org/packages/6f/98/d595fd2d231b6ad000c23f2d5b2d8927378b783cdc727b94906fd36fe7ac/xxhash-4.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8846f6f7fc91b31da17e65fa1388dc7564ab2a49705bafc7385685cb138d8c97", size = 35333, upload-time = "2026-08-12T16:54:45.926Z" }, + { url = "https://files.pythonhosted.org/packages/31/36/004f2c793ea27b380ca288237438bc39efe3e009158de7dcd2ebaea193a8/xxhash-4.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:281bee1d220ef2bcd98da244645b64b63ab4529639adfae08493149b683dca7c", size = 37702, upload-time = "2026-08-12T16:59:22.503Z" }, + { url = "https://files.pythonhosted.org/packages/36/77/a8a518eff9772f074a73fbe21b5bed8d6c2a2be9190a9e1ffd33f4565ca3/xxhash-4.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06c1d6011c7126da93a6ec1fdb587a9e672622721747287c49f0d9f8c74d8a2c", size = 37859, upload-time = "2026-08-12T16:55:31.76Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1c/03d2ef7526bea146d10247fbb546ac5c204374825e00f333e050034da9a9/xxhash-4.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63572670b457b18404aa2bf1e4eccf15b11897cb84c1d8d37f245ac3c0398564", size = 35579, upload-time = "2026-08-12T16:54:32.47Z" }, + { url = "https://files.pythonhosted.org/packages/82/d4/5586b3bb8564d48bcad8db26ded6fa441adf352348f794a20d35b163782e/xxhash-4.0.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f4ef8e1187013597c2ce1dd0f1eb03111d53b524ac4e31edbcbd44ca99c20ab", size = 259237, upload-time = "2026-08-12T16:54:38.665Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/de7da52bbb6efe85b609bc45fe31973f4cdc5abb3e66a20aca0d158dafb3/xxhash-4.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:945ec1b96f381c3646baf7a3a02dd9c5c2f1471f6433f3fe76f54dacdf865b4f", size = 283931, upload-time = "2026-08-12T16:55:03.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/ca/fa6b3dfba13123a4ba48e9c024275d46e6ab84f79836d25ea59cf4b264f5/xxhash-4.0.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0debc5d798cc976edec29e6b2b3b995144cabbcd1434a2b3a3132a3b7f72cd0", size = 303589, upload-time = "2026-08-12T16:59:14.762Z" }, + { url = "https://files.pythonhosted.org/packages/c3/af/834b2d3da35052307a7892b111e60d825b28ce7a5ebb400664a25f5795a6/xxhash-4.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:69b66cc631ad37f07e44ca04ec44eadcc86d0ecb9533c151e01df44c0741f2fe", size = 287097, upload-time = "2026-08-12T16:54:50.85Z" }, + { url = "https://files.pythonhosted.org/packages/31/b9/155d29cd9fae4fe3814cc072bfd4c2b45d1814f4c48c7b85e3850c9e7bf7/xxhash-4.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c1ad023a357ae1210f6d5fff4cca7273c08cf4adbf60cc6df243c262d3ce6a1b", size = 518895, upload-time = "2026-08-12T16:59:24.55Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/ad3a313fbaea9116cd33b1f1c454eb363012ce46933e1760ac072b1ee9f9/xxhash-4.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a2fcead5cdff26b002e8336cdec6f6eea588d350b1ec1010801878c9381779f", size = 267820, upload-time = "2026-08-12T16:55:36.44Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/dfa99ca585e2e7275f0fec3c93a2907a2e1f0a0498af1d320c425701dc15/xxhash-4.0.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:adc511931fda0114b05611b14474471b0a472ca9153fc347d30ed96fd628bbf7", size = 345169, upload-time = "2026-08-12T16:54:41.138Z" }, + { url = "https://files.pythonhosted.org/packages/98/b0/e6a00e115cb9c0c7a32375e23d4dc4b94e733e756766978a9c6fc7aa9cbe/xxhash-4.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:495820b8908edd87bb69924f04106dad7cb349763aacf741f820d7d0e04bb4da", size = 280231, upload-time = "2026-08-12T16:54:44.351Z" }, + { url = "https://files.pythonhosted.org/packages/a7/10/3855a7275d6bc04cb4ad31e5b44d91fc0f0ad2fcb6676ae56b313d4abbf6/xxhash-4.0.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:3cd39edf34945dd02f256a778eda5c9ea52e865a45f218460a5d6d12b266607f", size = 306797, upload-time = "2026-08-12T16:55:20.632Z" }, + { url = "https://files.pythonhosted.org/packages/f3/57/69ed901908fd34d264a5941fcccc83fb0886a6a6fefee8d40a25a3b1d59b/xxhash-4.0.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:02b998b2a3150780d596f9cf8ae90fc19e38c64971bddbe6e492de0c0c0d07e9", size = 265528, upload-time = "2026-08-12T16:59:17.822Z" }, + { url = "https://files.pythonhosted.org/packages/52/62/cfceb242c029a3198d4fb8b804c41b08d51e661aa43891a05a5839fc8562/xxhash-4.0.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:41c810a5ad4aaf862812575f477361e8a76a368764d3c265f53511fbbf8fa5ef", size = 284161, upload-time = "2026-08-12T16:55:07.446Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ca/1ec30ab71826decd231f1231e276f6cf0f54cae709e11427be8102b1843f/xxhash-4.0.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:91aa1d5418de8aaca5a418f22657a3e4c1bc37f993aba8b010a46b39f6d37501", size = 335726, upload-time = "2026-08-12T16:59:26.785Z" }, + { url = "https://files.pythonhosted.org/packages/31/ea/c710d5d1279c7c7e7ad4cf9d1d2f9106223e484a9b761a353cfcc5900a24/xxhash-4.0.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ef341c82fc06696c1feb6923c900e6e00a9f049f86d6bbc275cd6c02f7091c2a", size = 486311, upload-time = "2026-08-12T16:55:44.33Z" }, + { url = "https://files.pythonhosted.org/packages/6a/95/777a75e55e540da1c0fa83ab4ae6d517e047ba72a299d89f850d5d57bc76/xxhash-4.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb96c1c045996975892d55118f5fe323b1c7fb56ce1a5505f065e4c66e7f341c", size = 263414, upload-time = "2026-08-12T16:54:48.469Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/a5a0c97212574ff5bd9a297e11536e6caad705f0e9a554bf6e78ce2cafef/xxhash-4.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:82160fc93048d66bf7872b3e3d482f833d6c586e56357c53d9a4c147871a677e", size = 20200, upload-time = "2026-08-12T16:54:54.361Z" }, + { url = "https://files.pythonhosted.org/packages/df/da/b4de0e377f0f9bb732c9b7814492a249e668c3910b9a51df86a11ff5c4a2/xxhash-4.0.0-cp314-cp314-win32.whl", hash = "sha256:ca16817ba162e53c56e19c428f7629fbf86e0c36d6c95c1ae9f2206767a81f47", size = 34774, upload-time = "2026-08-12T16:55:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0a/4cebdfd9970a7859e0287621954270ca3ac63807b95e9d4dec316de13c80/xxhash-4.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:77dbca7da50340f2bddb1ab16ced523ae275f319d209362a7bdb7f306dd9cfdb", size = 36763, upload-time = "2026-08-12T16:59:19.774Z" }, + { url = "https://files.pythonhosted.org/packages/29/d7/d96b991a5fb945d6a6e54ae0bdda7502717a6d34640e4b00964682329f38/xxhash-4.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:e104176ad369e3d52cba2dd85832d19f9ad04393ccf738b260553e7a54a6fed6", size = 34036, upload-time = "2026-08-12T16:55:28.081Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/c1cc56e1fcbadcec54ce005e77e689a306299087320c4ddbc848af5d867b/xxhash-4.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9dccaddc24daee3c0aa0cfbaed95cdee594edf5638d941675628dc7bfbb22fc", size = 38329, upload-time = "2026-08-12T16:59:28.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/18/70afb6fc2c2e1bb4cea8387d3e4a8467678e6acb0786c88ebe704839f86c/xxhash-4.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8a00751c4b92c1f4b5785ef1729ae4ccebcffdba1ff4095a6c7285ab122e223e", size = 35958, upload-time = "2026-08-12T16:55:51.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/90/4bc82997ff15aeaedbb2c8a5e1b3908f8eba7599bf49c37ba5af71627a78/xxhash-4.0.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06eb2bdab339f0a59a1a6ad97c2be21d4dc4c2fa095182540bef56ccf1d149b1", size = 273387, upload-time = "2026-08-12T16:54:51.914Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ad/b881718ae097adedc2d03db5c7c30dce7da7f054f4e7bb7b7c3d1bf7e631/xxhash-4.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67284df807563ca3f79cf3ee0acc76ffa873aad85a9e587441d18d0cdb1fc143", size = 300757, upload-time = "2026-08-12T16:55:00.431Z" }, + { url = "https://files.pythonhosted.org/packages/f3/32/b7afbba0750a4792e59f81bcc0e9c105dd388c7977b6d3fbaab24641380a/xxhash-4.0.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3290769c98ea77b99d24017bccd72851079b15434478e6537034a8869f9b6fc4", size = 312404, upload-time = "2026-08-12T16:55:46Z" }, + { url = "https://files.pythonhosted.org/packages/a4/78/619630629a7e998c4a31b3111136420f15e715b72fda4fff71afa35da653/xxhash-4.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39046bb0cc10d7eb86805d4d2b0ea9a5d3b4fb03b9061a679e48b2c8651cd31a", size = 301087, upload-time = "2026-08-12T16:59:21.767Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a2/4269e599e9a7ab4b8f91529aee41c62a5c0cbc2624014cbc8a313ad082f9/xxhash-4.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:73774102f1cd53989d33ae340601139d6015a4ef4f9e38fd8fe47f24ab59472a", size = 534061, upload-time = "2026-08-12T16:55:46.98Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6c/4089a593bbf5c51b4d4750a0e2bdade5e3b1771f8e209d0e390f6892de79/xxhash-4.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445a802761e2d6f0ca4741464fbbb08e21e3a7885845e930bca4c8faf48fde2", size = 279278, upload-time = "2026-08-12T16:59:30.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/05/f544e0db4fffe64dd25de640152a64f341c4a9a915108d507bbc873c6c62/xxhash-4.0.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:513304b55d0f857066548492005c2edd332ca88bdfa36a53619fe09f4f582dc5", size = 358600, upload-time = "2026-08-12T16:55:56.265Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/ce6853d35227f16d7cee4c231e6dd79fc50725808dbdd18f74e5f5f239d9/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:124d503e5ac068a0977dbfd3cbf906043bb377e223f6fda76695fe070a5e2acf", size = 295001, upload-time = "2026-08-12T16:55:06.447Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/91bb0d5be29e60284d78d5769a80247e55480015fd8be67e4c9e7c0a2496/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fbc07b879e9e8290a42556914234b5ae149c00c2e2c95ae1fbeedf96785c0f89", size = 319835, upload-time = "2026-08-12T16:55:10.564Z" }, + { url = "https://files.pythonhosted.org/packages/b7/60/5f9792b182848e7fe24acc81ff3d48f4729416361e79c1e4c30202183580/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:310c51cbf02be5ee7fc70a40c3d8fb609ab9efc83d0371eebdb9158b9324c0a9", size = 279061, upload-time = "2026-08-12T16:55:52.714Z" }, + { url = "https://files.pythonhosted.org/packages/60/e0/88c48e6de56b33182fae9817d67a7f3276cc6a41afc6abfd884adb1fc803/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d96feacc843a42644ba5644f8119c2f0696fc3e0164b8d85dc22a3b702fdc2f1", size = 297263, upload-time = "2026-08-12T16:59:23.682Z" }, + { url = "https://files.pythonhosted.org/packages/30/2c/dcd5f06fed249a41a785d83d8882759e0792258a40717808a7a38fe864bc/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a43ff13f62a6c0afa04464ed1932a8fb2dcf1cd24ac01c3b90e82d3cf22d4c3d", size = 348133, upload-time = "2026-08-12T16:55:53.517Z" }, + { url = "https://files.pythonhosted.org/packages/eb/54/aeac468983c72d71c4299b0eabbec9264685bc82d5f78c56b5336097c1f9/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:471a2122c59d05accf9001f1d8dabca473faf1caf01081b5226506d823468e3b", size = 498970, upload-time = "2026-08-12T16:59:33.199Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2a/707b28087bf5b196753897680c20a3ae7ab8f60226cc8a1ca2b63efbf9f8/xxhash-4.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:78b4fc6bb9bac999eb1ff141dc597aed3b0ddadf52f572211f0f17acfc2b844e", size = 274693, upload-time = "2026-08-12T16:56:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/51/6c/80f952b5badf300b11469af73bcff7928017f27d6143ba4fa999708b96fd/xxhash-4.0.0-cp314-cp314t-win32.whl", hash = "sha256:60fdebd3eb6da7ef6b5e2b36105dc0711d1419f7c45caa0cd14244119315be65", size = 35125, upload-time = "2026-08-12T16:55:11.3Z" }, + { url = "https://files.pythonhosted.org/packages/81/f6/ce91b1e06c24bcb9ae24cdf0481bc0cb39e41c98d61631b09659501b6cf9/xxhash-4.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8920c21c73dadf9e5bee1e13e250a1acf63f3dd6d609ab2ba5d4abbd86394be2", size = 37103, upload-time = "2026-08-12T16:55:21.46Z" }, + { url = "https://files.pythonhosted.org/packages/9b/95/860bed3a0c7dede4a6a1734ab6f88998bef3e20a3d5ccf2c169073aaff9f/xxhash-4.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:55c026944fd593b4199fa299f7667aa79622d625e30f638f41f92f0c192c90d8", size = 34254, upload-time = "2026-08-12T16:55:58.587Z" }, + { url = "https://files.pythonhosted.org/packages/98/05/4e30f443ae181d91a112d22dad9e34e60028a52cd8afd9d41c1c237b419d/xxhash-4.0.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:63593a631535b205f0e5fdad0121cd57366c00b7b00846dbe9de513051c03796", size = 43328, upload-time = "2026-08-12T16:59:25.621Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5c/a6ef6ce5c3a8a3132931d0eec3cb2a711a75355611be786009867ba113f5/xxhash-4.0.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:c9515e4babe412b06e0f9e46b806b7b2b9ce946da90e9ab0130758dcdaba74a5", size = 40396, upload-time = "2026-08-12T16:56:04.305Z" }, + { url = "https://files.pythonhosted.org/packages/52/6f/e80ae3fd446c9f3ad9933afc24e9a49fcfacdb1097ef4eecc5bf0b7786aa/xxhash-4.0.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fb8787ce65e0358af457353fdb261cb89382670ec87cf2c0dbddcded165388f6", size = 34443, upload-time = "2026-08-12T16:59:35.176Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/41a0d46d01488d362bfc5ce94b57a3422e5a74543306cc06aad968f58285/xxhash-4.0.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:c97fc349108401ca73a0b7e625fd06ed8d5ae94aa9e7285eb8c60497d448eb1a", size = 35436, upload-time = "2026-08-12T16:56:10.105Z" }, + { url = "https://files.pythonhosted.org/packages/56/4f/7a89b14701f53f9832b01012c3c5ff580118c89ae6a48ac68aa0444b41d5/xxhash-4.0.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:f43682b8f65b7571f90ac125feb87a1382a4d37f9dc41b2b759fc4b2a8ee20f2", size = 37711, upload-time = "2026-08-12T16:55:23.451Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/5fdaf0d29f9ac2bb0945d4c88aaa64f1abac40e788f3f2c64f8c374db80d/xxhash-4.0.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:3f71e14e5bca3ffa97c728463ccad14ee939bbf9c4224ccd4b3c422ef8aa2791", size = 37858, upload-time = "2026-08-12T16:55:42.989Z" }, + { url = "https://files.pythonhosted.org/packages/e1/64/6acc8a32f0ca5cbc581a1cf62352cd0e46f27ee090d10906fa51ba8b934f/xxhash-4.0.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3ee15fb91280eed4f63386350e06be33589a366004cc6467a1599c36f746d078", size = 35677, upload-time = "2026-08-12T16:56:02.914Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/9f1c3bdc4645855f44c63e87a46909ceb209ec8ff9ef9863c94846cd58bf/xxhash-4.0.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6d2f619722866cc583f40a756f54528749a5bce6a7a80fda7a5dff868b992967", size = 264256, upload-time = "2026-08-12T16:59:27.837Z" }, + { url = "https://files.pythonhosted.org/packages/c1/0e/6496112e775198b79dc2a8e2cf3fc095844275af2ad4b2d76ec549ada5db/xxhash-4.0.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5398ba89021d4066890c3333e2e167a038fab8570580177b9cd0c3386eb47661", size = 285579, upload-time = "2026-08-12T16:56:07.755Z" }, + { url = "https://files.pythonhosted.org/packages/84/c2/e147637f8f35572571a371c6e7631e9a3d0efcbed42ee61aba40e451613f/xxhash-4.0.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b5e8b65a881aaecbd25509df36629f1ab492f362424f2dad0c57e4127d1a1083", size = 306214, upload-time = "2026-08-12T16:59:37.296Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ab/4fcce21d7be3c6cf18d2ea31bd2b602ed43469fbcdba0186548f05576daf/xxhash-4.0.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:efdad32c578f66ca0845034d4f271b437eb171e97bed7bd273354f37ef4da8ae", size = 287578, upload-time = "2026-08-12T16:56:24.811Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d2/11ddb15c27d3511de94baba2855d66a7cc40e918db2c582dc8b36e0fd8bb/xxhash-4.0.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3acc4dba6fc4b1f4f70ef5464bba460eda3058fd3a109c424ae44291dc50fe0", size = 519722, upload-time = "2026-08-12T16:55:32.81Z" }, + { url = "https://files.pythonhosted.org/packages/54/ff/d38c42cf1d34486cd85147c3178e04ddd01d4318b8e07316709f2d70800b/xxhash-4.0.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ac51bc651472066723721eb59508aa4f3b2611ecc0b4773ab60d2681a639cb2", size = 267907, upload-time = "2026-08-12T16:55:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/3f38a93627e6430583e3976be4344b48dc8242a50b917fd480b394e06008/xxhash-4.0.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f1d54c84bfb67e063636fd653cb9a2ffb7e647de8da1004a351a27dff9de9669", size = 349861, upload-time = "2026-08-12T16:56:06.996Z" }, + { url = "https://files.pythonhosted.org/packages/ed/46/4090deeca4004de6dfe08cbd87f55f4bf086762aa03ccf487c66b484441b/xxhash-4.0.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:407d0e39e477c414a86050c4cc2831a628d1b8948c9e2bd73c49585d6e99cddc", size = 281668, upload-time = "2026-08-12T16:59:30.107Z" }, + { url = "https://files.pythonhosted.org/packages/c2/37/599c6c59395a64e8068b3c00586e882017b5f732a7d6c02104cad3c735cb/xxhash-4.0.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:22bc76799e668c24a73f113a6621104fc1a41d85c007f755f57f5b3a43a8bb62", size = 307435, upload-time = "2026-08-12T16:56:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/35a79ff77f9d216064b8c959db7ecb5abdb411b4427cb7ca1eb9d8eead0f/xxhash-4.0.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:a7114d66fa58deb1c7bd1de3ee41b046bbf2db0ab972f87d9e627e3cbf0c1308", size = 267888, upload-time = "2026-08-12T16:59:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5d/f0cdf4312c4689075015d0e6e6d9fcc21a805c6d036e8d07166ed33d188e/xxhash-4.0.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:54610f8a2e506fa9c6ea5fc2b1082638ef9fd747b7aed70fe5b1cbc281e93f31", size = 284599, upload-time = "2026-08-12T16:56:31.067Z" }, + { url = "https://files.pythonhosted.org/packages/d2/be/b01ff7e9e05bd17380369b4f5ffa562ac12831b856416ce6233196d7b045/xxhash-4.0.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ac951a9627034fd56864614036e9866d21d94a3ce203dafb0997ebf8e8895987", size = 337778, upload-time = "2026-08-12T16:55:45.097Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2a/062cd6954140e695ed3e04e856dbfe4e85047d52bede53adb0c9a908c757/xxhash-4.0.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:475bf58602738085a515c1279263964ddb7c3d833dad4eda07e79f56f885b970", size = 487351, upload-time = "2026-08-12T16:55:56.829Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e4/0424e7da6cdbedd1cbc82b3b8adbec379d5c4c0a5a48bd65bffa384e83f2/xxhash-4.0.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:aa6ae5421223693063c04b32456bd64c9b5ef115f7c4c9e19fd8c4a145b1df54", size = 263520, upload-time = "2026-08-12T16:56:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/92/21/d60db12cae912f10063ed24c3ce32012efd3f1e0ef747218cc4e1efed45a/xxhash-4.0.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:2b204025a20bc1d2a3e55a741ec83efbcef59d5666784a4a16568217818afa08", size = 20208, upload-time = "2026-08-12T16:59:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1e/12e94b5941559d16070a119ceacdd33e97202f048f8d1d750e1ee8db7fc2/xxhash-4.0.0-cp315-cp315-win32.whl", hash = "sha256:b15607539c9697b60052672badbd08699b691f47273c4a88f2b2c698ef28ba48", size = 34767, upload-time = "2026-08-12T16:56:23.447Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b0/9d639d32a688e25acf27853d9e87a17a3bfb93f70a6970dc269f0b360eeb/xxhash-4.0.0-cp315-cp315-win_amd64.whl", hash = "sha256:e0e4e03bf2f53eb00e357ba351c909cb5f70f6bc92aa741a079c9663082b1f8e", size = 36755, upload-time = "2026-08-12T16:59:42.614Z" }, + { url = "https://files.pythonhosted.org/packages/51/7c/cc8b681c59996b4893fdecbc28f378afeb877d1404e11327fac627e771e9/xxhash-4.0.0-cp315-cp315-win_arm64.whl", hash = "sha256:5cee6e02c4f9295da82b783fdbb70b78c6417e2c9c7f33073426bf3b3f9bb046", size = 34031, upload-time = "2026-08-12T16:56:38.941Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7e/1274750be96bbddb5dfa1985de47dff8d5a5974207ca8e101b459bc9ea12/xxhash-4.0.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c38e2792b322a52444be509e81492ef839d1134227a0771366012a11256a026f", size = 38294, upload-time = "2026-08-12T16:55:50.571Z" }, + { url = "https://files.pythonhosted.org/packages/fb/88/8a10e2c0f741404a8787cbcb3a95f02591d5237fa97ff6b3d8407716fd6e/xxhash-4.0.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3b5fa8181a65d3060ebf94712b34c6e68266c1119d245f959c3b1c2dc78800b6", size = 36029, upload-time = "2026-08-12T16:58:56.913Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f2/854a196948bde98e03919a711eeb34722fffcb1f5495f39ad03fff43fe04/xxhash-4.0.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c099fb5a46aa9781a4f6115cfbf799da1e1309ba880237911a9ce4626c113a9", size = 273203, upload-time = "2026-08-12T16:56:25.389Z" }, + { url = "https://files.pythonhosted.org/packages/81/52/bd810d07e8cd02457fc92c204a11363f295b313b4838bdbeb76aecf500fd/xxhash-4.0.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b262c895ed923b75ff2cb4f3137d7f4eeae3d8295ea6d86a66f6190e88a792c3", size = 298792, upload-time = "2026-08-12T16:59:34.074Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4e/af6d84158955780b7c3874293ca5be3e105686e68db1f63a121291715c6b/xxhash-4.0.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6f7b3839b8a407c666fb37f8e4235f3274abaf3c561038613b9300ddd3152667", size = 314127, upload-time = "2026-08-12T16:56:26.035Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f0/d1cb1537789715f399f529202eff4ed61a64af842943964d71f9661162fd/xxhash-4.0.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:495342265b1495c754c497a1bae03a4d081877443883a04c3163d5f34e5fdfa9", size = 298786, upload-time = "2026-08-12T16:59:44.776Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7d/3f11597cd9b28a02e5085fc68eb0c6363d3f52caa588643123c31d0c2112/xxhash-4.0.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1354501ba9cfde53fc2f64a685713fc0b457d940b911f7053eb5062733423eb6", size = 531582, upload-time = "2026-08-12T16:56:45.603Z" }, + { url = "https://files.pythonhosted.org/packages/a9/18/4ca9c34ea79a5f8f32858c0ee44771e4c9cc640104eba69e1582ad888d1d/xxhash-4.0.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86ba859f8c9fb5eea9f3452beb692e88eaa19fcd72dcb1a4ab4bf61d1ef7a349", size = 278440, upload-time = "2026-08-12T16:55:55.305Z" }, + { url = "https://files.pythonhosted.org/packages/2d/2f/53647ce0acde9f3aa053579e8ecfcd78c2db8c67cef6cff50363e1e30e43/xxhash-4.0.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bcad0042c4b7b4384af9758bc1b843e574cc28002fef5808b907b8a6681cfa65", size = 359572, upload-time = "2026-08-12T16:58:58.379Z" }, + { url = "https://files.pythonhosted.org/packages/5d/80/33d6a144e17464d244209dc8e04962420bc942ecc6e2db0e41f517c0d20a/xxhash-4.0.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:4341e6acffd0f7d407e82fa76081ce4c886282934906f7bea64f646536e0d463", size = 293285, upload-time = "2026-08-12T16:56:32.162Z" }, + { url = "https://files.pythonhosted.org/packages/2e/52/1b7c25d08b22e5d28e59119c455693152bb61a636ee8f6e9679e05eeae54/xxhash-4.0.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:953bffb74816c878d3df107e7bffb3b2de61069bec2bf7983712c5427739d340", size = 318408, upload-time = "2026-08-12T16:59:36.507Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/6bf95568247564cc08995277b1d993cb3920168044d61cbb9e3d681d34ae/xxhash-4.0.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:7e221d1ea5ec85a96c0946c22c31866a83c53dbd1a9222775e2e04254bb836aa", size = 277241, upload-time = "2026-08-12T16:56:29.585Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1a/b7d75c8e2b35a3a57de1468204e8e313e896039bcc98e01a2f778731a2d0/xxhash-4.0.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af2b9b10d6623e472cd671b7fc060fbb8d336c9c3504ad7b2bf690ccbb429f1", size = 294684, upload-time = "2026-08-12T16:59:47.04Z" }, + { url = "https://files.pythonhosted.org/packages/76/73/cb83c3393cdd4d9025bfe3b3ae06979a4c0d571c6720b57a40233d8ff1cd/xxhash-4.0.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f48f64db93c91259d8c32839ba3be1b3fa11b4cc451791e0144fcebd7aaaaf2a", size = 346706, upload-time = "2026-08-12T16:56:52.302Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d7/6547d8efb8bf04063c9bd60dfd17e8afbadad56e7cb4e9663f862343abef/xxhash-4.0.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:4a93db301eb87c1df0ff353f4c5a66571cb7473480cc9e32a4cdebc39f2c8b59", size = 496910, upload-time = "2026-08-12T16:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/04030c5f759344c63f1e796f3f756a90f02f56b73fe0008ef56dba058e83/xxhash-4.0.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c442f105adf731ae096cbf8830079739d3f6dec2b76809e3c7c0a6a42fe78fa3", size = 273978, upload-time = "2026-08-12T16:58:59.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/6c6e9d182b23d6301d1543b897eca975e796cb23536bf1d70bcc82656871/xxhash-4.0.0-cp315-cp315t-win32.whl", hash = "sha256:c16d66337724cf23e753e72d967646a1236127701cf6ec5b5ccf82af3d2932c9", size = 35115, upload-time = "2026-08-12T16:56:36.293Z" }, + { url = "https://files.pythonhosted.org/packages/d4/62/b4383d7481380a8428abeb969b91324cc2b6fa8c48f747c2d4d1f8cf5cf7/xxhash-4.0.0-cp315-cp315t-win_amd64.whl", hash = "sha256:5fe3e37e5c00a265deb33e233b94e468f38d9325f1c62e05de3ff768c4237702", size = 37094, upload-time = "2026-08-12T16:59:39.284Z" }, + { url = "https://files.pythonhosted.org/packages/52/3b/76ff969e59ce6491a9aba99b5b06a52b82684de637d0c22cdda9aba36292/xxhash-4.0.0-cp315-cp315t-win_arm64.whl", hash = "sha256:2cabef18c8353c1e28d4c3f8ba030e1892799d53bd83fa31f0ddb3e590e6aa7f", size = 34246, upload-time = "2026-08-12T16:56:35.428Z" }, + { url = "https://files.pythonhosted.org/packages/dc/50/525619de9e4ec98f5738ee4a0aa6177bc297bea5e53288080e5c15f377d2/xxhash-4.0.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:cc563b697d5eefc6a0a699203076687ec87db4ef8fd03330786343e309a6c009", size = 31502, upload-time = "2026-08-12T16:59:45.85Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1b/1718da1d831ce7b00b6ea69fa7f19b52a1a700ac32f8b913a799ee0bdde7/xxhash-4.0.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:9a9d55a4dd7e6693cc1e15119b0719d446495513b06ed020bec8ff77a84288f0", size = 34116, upload-time = "2026-08-12T16:56:51.108Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6d/7ffeb80640ecafdc51a0de3096f450891d1c3baaf77543f49a1451a844a7/xxhash-4.0.0-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07b899cf57f2573019a064b66c25b20d3e6934273fb8b921f96d74f417032d87", size = 38163, upload-time = "2026-08-12T16:59:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/9d/88/51ff39be48d02e223942127189359397f87301c6c5f12efb9fda2d7ac8d6/xxhash-4.0.0-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:affef90b19eb63d054a1c16df70daee553dd735e9109e3ded5f390dda7e1eab4", size = 37885, upload-time = "2026-08-12T16:57:06.59Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/cd7d58dc604a395d33f5b73773caba6fdfca7e20ab54087094655c1a474b/xxhash-4.0.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:56ab134b8a5ef7a4d95c8cc0436ccab03ab20cab799e0886f4f1e728e205893e", size = 36816, upload-time = "2026-08-12T16:56:32.76Z" }, + { url = "https://files.pythonhosted.org/packages/69/b9/27ce26769194c38e93f9b6a5cc8e6cc24b976e3221566efe89c3db055064/xxhash-4.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e0690932bd17f1e2e0895d44844fc3bc33304c64970dbdc8e10e86a2808fe40", size = 36099, upload-time = "2026-08-12T16:59:08.061Z" }, + { url = "https://files.pythonhosted.org/packages/49/e1/f8d8f3ade77145e937baf7c62b038c964f248a33a439406413ca7d82d5fb/xxhash-4.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ec6f32520a939345f057fe4fd4cba9e988caf9aa738ae0648ebc9a0e26f9afba", size = 33104, upload-time = "2026-08-12T16:56:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/48/7d/6560dc2540d83e885657ce9a44afed7173f71204a8877480853790444f36/xxhash-4.0.0-pp310-pypy310_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:131e68f730eade01b60b37df1017d07a804ebc62dd06cc9fd6e241c6c09628da", size = 47684, upload-time = "2026-08-12T16:59:47.883Z" }, + { url = "https://files.pythonhosted.org/packages/53/40/23edf739346d4b7c38c684d7b7a8089549118f423dba8fcff0649f67a11a/xxhash-4.0.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a870ba5809434c0e33bc1c996fbadf59f5bee2346e3ae212e74a1a8a5389a38", size = 42366, upload-time = "2026-08-12T16:56:53.662Z" }, + { url = "https://files.pythonhosted.org/packages/5c/07/e46c7ba805c3f7584d59575a185cec04cbd351f694bb7542400129230dee/xxhash-4.0.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebf1b7d63171a3ffd8a7361142a51fb7463defa43068d787bb3aa05e0ac822e1", size = 39118, upload-time = "2026-08-12T16:59:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/98/3e/89d28986f30a11f029d07164916634062ff77fe4f5a84dbefa6ac29256ed/xxhash-4.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ade38cd596f12a250b747a2bb54a7e23b45dcc112c537d34e08e16b06d09bf31", size = 36881, upload-time = "2026-08-12T16:57:07.893Z" }, + { url = "https://files.pythonhosted.org/packages/83/b1/49cb67d760607af93db044208df1ae51d667e74e9352bdf0a79559445ce0/xxhash-4.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a1c8a2ea77ad2b22ab4ad34cc673aedd291143169ce10fac5728f5d3c017b378", size = 35993, upload-time = "2026-08-12T16:56:37.708Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1a/d2855a8bb2f5a7ae10ffd55621a17bef3021646e8b1095494c678f6ace80/xxhash-4.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1969258533437c96c8a46028820e9c99741ad7d03476abcb76566e2e324d89", size = 33186, upload-time = "2026-08-12T16:59:10.926Z" }, + { url = "https://files.pythonhosted.org/packages/75/fd/9b6cafb5bcaf99442b5ac0ed2898c830cea9d2f9d962f7f732a17677771e/xxhash-4.0.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8f3808b745a46aebdc844c603c6b9ab550451cbb499f706decd76f7d1a65ca4b", size = 47649, upload-time = "2026-08-12T16:57:00.637Z" }, + { url = "https://files.pythonhosted.org/packages/94/d0/0f527d9b2b5b62ae3fed9c28c870d06e3f68926fed465f0b53f2322e6985/xxhash-4.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:627149915a90215c694e4ac0c75bad8ae00eee1937f39f45b4ff46c01130fdee", size = 42388, upload-time = "2026-08-12T16:59:50.026Z" }, + { url = "https://files.pythonhosted.org/packages/3f/9c/ec4801ca684181b0f8bcea447721bc007b740a9ae5c15128e3aed481e51b/xxhash-4.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:65a6af70b5f5ed88f88791c5de70905c655cff0fee062604f67acfc843c2841e", size = 39184, upload-time = "2026-08-12T16:56:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/8f/43/efbc17c2a467da34f207dcef369cc9364aa2a8172b4e056a0e875e934688/xxhash-4.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:36f59fa0d52d984a19c3026a951a183ebabfb2a847f9c5addc43e0191d44c3ab", size = 36911, upload-time = "2026-08-12T16:59:58.257Z" }, +] + [[package]] name = "zipp" version = "4.1.0" @@ -1739,3 +2456,93 @@ 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/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, + { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, + { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, + { 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" }, +]